Passed
Pull Request — release-2.1 (#7124)
by Jon
04:15
created

normalize_iri()   B

Complexity

Conditions 7

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 7
eloc 15
c 2
b 0
f 0
nop 1
dl 0
loc 29
rs 8.8333
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_iri($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
					$data[0] = normalize_iri($data[0]);
1818
1819
					$scheme = parse_iri($data[0], PHP_URL_SCHEME);
1820
					if (empty($scheme))
1821
						$data[0] = '//' . ltrim($data[0], ':/');
1822
1823
					$ascii_url = iri_to_url($data[0]);
1824
					if ($ascii_url !== $data[0])
1825
						$tag['content'] = str_replace('href="$1"', 'href="' . $ascii_url . '"', $tag['content']);
1826
				},
1827
			),
1828
			array(
1829
				'tag' => 'float',
1830
				'type' => 'unparsed_equals',
1831
				'test' => '(left|right)(\s+max=\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)?\]',
1832
				'before' => '<div $1>',
1833
				'after' => '</div>',
1834
				'validate' => function(&$tag, &$data, $disabled)
1835
				{
1836
					$class = 'class="bbc_float float' . (strpos($data, 'left') === 0 ? 'left' : 'right') . '"';
1837
1838
					if (preg_match('~\bmax=(\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)~', $data, $matches))
1839
						$css = ' style="max-width:' . $matches[1] . (is_numeric($matches[1]) ? 'px' : '') . '"';
1840
					else
1841
						$css = '';
1842
1843
					$data = $class . $css;
1844
				},
1845
				'trim' => 'outside',
1846
				'block_level' => true,
1847
			),
1848
			// Legacy (alias of [url] with an FTP URL)
1849
			array(
1850
				'tag' => 'ftp',
1851
				'type' => 'unparsed_content',
1852
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
1853
				'validate' => function(&$tag, &$data, $disabled)
1854
				{
1855
					$data = normalize_iri(strtr($data, array('<br>' => '')));
1856
1857
					$scheme = parse_iri($url, PHP_URL_SCHEME);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $url seems to be never defined.
Loading history...
1858
					if (empty($scheme))
1859
						$url = 'ftp://' . ltrim($url, ':/');
0 ignored issues
show
Unused Code introduced by
The assignment to $url is dead and can be removed.
Loading history...
1860
1861
					$ascii_url = iri_to_url($data);
1862
					if ($ascii_url !== $data)
1863
						$tag['content'] = str_replace('href="$1"', 'href="' . $ascii_url . '"', $tag['content']);
1864
				},
1865
			),
1866
			// Legacy (alias of [url] with an FTP URL)
1867
			array(
1868
				'tag' => 'ftp',
1869
				'type' => 'unparsed_equals',
1870
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
1871
				'after' => '</a>',
1872
				'validate' => function(&$tag, &$data, $disabled)
1873
				{
1874
					$data = iri_to_url($data);
1875
1876
					$scheme = parse_iri($data, PHP_URL_SCHEME);
1877
					if (empty($scheme))
1878
						$data = 'ftp://' . ltrim($data, ':/');
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type boolean; however, parameter $string of ltrim() 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

1878
						$data = 'ftp://' . ltrim(/** @scrutinizer ignore-type */ $data, ':/');
Loading history...
1879
				},
1880
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1881
				'disabled_after' => ' ($1)',
1882
			),
1883
			array(
1884
				'tag' => 'font',
1885
				'type' => 'unparsed_equals',
1886
				'test' => '[A-Za-z0-9_,\-\s]+?\]',
1887
				'before' => '<span style="font-family: $1;" class="bbc_font">',
1888
				'after' => '</span>',
1889
			),
1890
			// Legacy (one of those things that should not be done)
1891
			array(
1892
				'tag' => 'glow',
1893
				'type' => 'unparsed_commas',
1894
				'test' => '[#0-9a-zA-Z\-]{3,12},([012]\d{1,2}|\d{1,2})(,[^]]+)?\]',
1895
				'before' => '<span style="text-shadow: $1 1px 1px 1px">',
1896
				'after' => '</span>',
1897
			),
1898
			// Legacy (alias of [color=green])
1899
			array(
1900
				'tag' => 'green',
1901
				'before' => '<span style="color: green;" class="bbc_color">',
1902
				'after' => '</span>',
1903
			),
1904
			array(
1905
				'tag' => 'html',
1906
				'type' => 'unparsed_content',
1907
				'content' => '<div>$1</div>',
1908
				'block_level' => true,
1909
				'disabled_content' => '$1',
1910
			),
1911
			array(
1912
				'tag' => 'hr',
1913
				'type' => 'closed',
1914
				'content' => '<hr>',
1915
				'block_level' => true,
1916
			),
1917
			array(
1918
				'tag' => 'i',
1919
				'before' => '<i>',
1920
				'after' => '</i>',
1921
			),
1922
			array(
1923
				'tag' => 'img',
1924
				'type' => 'unparsed_content',
1925
				'parameters' => array(
1926
					'alt' => array('optional' => true),
1927
					'title' => array('optional' => true),
1928
					'width' => array('optional' => true, 'value' => ' width="$1"', 'match' => '(\d+)'),
1929
					'height' => array('optional' => true, 'value' => ' height="$1"', 'match' => '(\d+)'),
1930
				),
1931
				'content' => '$1',
1932
				'validate' => function(&$tag, &$data, $disabled, $params)
1933
				{
1934
					$url = iri_to_url(strtr($data, array('<br>' => '')));
1935
1936
					if (parse_iri($url, PHP_URL_SCHEME) === null)
1937
						$url = '//' . ltrim($url, ':/');
1938
					else
1939
						$url = get_proxied_url($url);
1940
1941
					$alt = !empty($params['{alt}']) ? ' alt="' . $params['{alt}']. '"' : ' alt=""';
1942
					$title = !empty($params['{title}']) ? ' title="' . $params['{title}']. '"' : '';
1943
1944
					$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">';
1945
				},
1946
				'disabled_content' => '($1)',
1947
			),
1948
			array(
1949
				'tag' => 'iurl',
1950
				'type' => 'unparsed_content',
1951
				'content' => '<a href="$1" class="bbc_link">$1</a>',
1952
				'validate' => function(&$tag, &$data, $disabled)
1953
				{
1954
					$data = normalize_iri(strtr($data, array('<br>' => '')));
1955
1956
					$scheme = parse_iri($data, PHP_URL_SCHEME);
1957
					if (empty($scheme))
1958
						$data = '//' . ltrim($data, ':/');
1959
1960
					$ascii_url = iri_to_url($data);
1961
					if ($ascii_url !== $data)
1962
						$tag['content'] = str_replace('href="$1"', 'href="' . $ascii_url . '"', $tag['content']);
1963
				},
1964
			),
1965
			array(
1966
				'tag' => 'iurl',
1967
				'type' => 'unparsed_equals',
1968
				'quoted' => 'optional',
1969
				'before' => '<a href="$1" class="bbc_link">',
1970
				'after' => '</a>',
1971
				'validate' => function(&$tag, &$data, $disabled)
1972
				{
1973
					if (substr($data, 0, 1) == '#')
1974
						$data = '#post_' . substr($data, 1);
1975
					else
1976
					{
1977
						$data = iri_to_url($data);
1978
1979
						$scheme = parse_iri($data, PHP_URL_SCHEME);
1980
						if (empty($scheme))
1981
							$data = '//' . ltrim($data, ':/');
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type boolean; however, parameter $string of ltrim() 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

1981
							$data = '//' . ltrim(/** @scrutinizer ignore-type */ $data, ':/');
Loading history...
1982
					}
1983
				},
1984
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1985
				'disabled_after' => ' ($1)',
1986
			),
1987
			array(
1988
				'tag' => 'justify',
1989
				'before' => '<div class="justifytext">',
1990
				'after' => '</div>',
1991
				'block_level' => true,
1992
			),
1993
			array(
1994
				'tag' => 'left',
1995
				'before' => '<div class="lefttext">',
1996
				'after' => '</div>',
1997
				'block_level' => true,
1998
			),
1999
			array(
2000
				'tag' => 'li',
2001
				'before' => '<li>',
2002
				'after' => '</li>',
2003
				'trim' => 'outside',
2004
				'require_parents' => array('list'),
2005
				'block_level' => true,
2006
				'disabled_before' => '',
2007
				'disabled_after' => '<br>',
2008
			),
2009
			array(
2010
				'tag' => 'list',
2011
				'before' => '<ul class="bbc_list">',
2012
				'after' => '</ul>',
2013
				'trim' => 'inside',
2014
				'require_children' => array('li', 'list'),
2015
				'block_level' => true,
2016
			),
2017
			array(
2018
				'tag' => 'list',
2019
				'parameters' => array(
2020
					'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)'),
2021
				),
2022
				'before' => '<ul class="bbc_list" style="list-style-type: {type};">',
2023
				'after' => '</ul>',
2024
				'trim' => 'inside',
2025
				'require_children' => array('li'),
2026
				'block_level' => true,
2027
			),
2028
			array(
2029
				'tag' => 'ltr',
2030
				'before' => '<bdo dir="ltr">',
2031
				'after' => '</bdo>',
2032
				'block_level' => true,
2033
			),
2034
			array(
2035
				'tag' => 'me',
2036
				'type' => 'unparsed_equals',
2037
				'before' => '<div class="meaction">* $1 ',
2038
				'after' => '</div>',
2039
				'quoted' => 'optional',
2040
				'block_level' => true,
2041
				'disabled_before' => '/me ',
2042
				'disabled_after' => '<br>',
2043
			),
2044
			array(
2045
				'tag' => 'member',
2046
				'type' => 'unparsed_equals',
2047
				'before' => '<a href="' . $scripturl . '?action=profile;u=$1" class="mention" data-mention="$1">@',
2048
				'after' => '</a>',
2049
			),
2050
			// Legacy (horrible memories of the 1990s)
2051
			array(
2052
				'tag' => 'move',
2053
				'before' => '<marquee>',
2054
				'after' => '</marquee>',
2055
				'block_level' => true,
2056
				'disallow_children' => array('move'),
2057
			),
2058
			array(
2059
				'tag' => 'nobbc',
2060
				'type' => 'unparsed_content',
2061
				'content' => '$1',
2062
			),
2063
			array(
2064
				'tag' => 'php',
2065
				'type' => 'unparsed_content',
2066
				'content' => '<span class="phpcode">$1</span>',
2067
				'validate' => isset($disabled['php']) ? null : function(&$tag, &$data, $disabled)
2068
				{
2069
					if (!isset($disabled['php']))
2070
					{
2071
						$add_begin = substr(trim($data), 0, 5) != '&lt;?';
2072
						$data = highlight_php_code($add_begin ? '&lt;?php ' . $data . '?&gt;' : $data);
2073
						if ($add_begin)
2074
							$data = preg_replace(array('~^(.+?)&lt;\?.{0,40}?php(?:&nbsp;|\s)~', '~\?&gt;((?:</(font|span)>)*)$~'), '$1', $data, 2);
2075
					}
2076
				},
2077
				'block_level' => false,
2078
				'disabled_content' => '$1',
2079
			),
2080
			array(
2081
				'tag' => 'pre',
2082
				'before' => '<pre>',
2083
				'after' => '</pre>',
2084
			),
2085
			array(
2086
				'tag' => 'quote',
2087
				'before' => '<blockquote><cite>' . $txt['quote'] . '</cite>',
2088
				'after' => '</blockquote>',
2089
				'trim' => 'both',
2090
				'block_level' => true,
2091
			),
2092
			array(
2093
				'tag' => 'quote',
2094
				'parameters' => array(
2095
					'author' => array('match' => '(.{1,192}?)', 'quoted' => true),
2096
				),
2097
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
2098
				'after' => '</blockquote>',
2099
				'trim' => 'both',
2100
				'block_level' => true,
2101
			),
2102
			array(
2103
				'tag' => 'quote',
2104
				'type' => 'parsed_equals',
2105
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': $1</cite>',
2106
				'after' => '</blockquote>',
2107
				'trim' => 'both',
2108
				'quoted' => 'optional',
2109
				// Don't allow everything to be embedded with the author name.
2110
				'parsed_tags_allowed' => array('url', 'iurl', 'ftp'),
2111
				'block_level' => true,
2112
			),
2113
			array(
2114
				'tag' => 'quote',
2115
				'parameters' => array(
2116
					'author' => array('match' => '([^<>]{1,192}?)'),
2117
					'link' => array('match' => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|msg=\d+?|action=profile;u=\d+)'),
2118
					'date' => array('match' => '(\d+)', 'validate' => 'timeformat'),
2119
				),
2120
				'before' => '<blockquote><cite><a href="' . $scripturl . '?{link}">' . $txt['quote_from'] . ': {author} ' . $txt['search_on'] . ' {date}</a></cite>',
2121
				'after' => '</blockquote>',
2122
				'trim' => 'both',
2123
				'block_level' => true,
2124
			),
2125
			array(
2126
				'tag' => 'quote',
2127
				'parameters' => array(
2128
					'author' => array('match' => '(.{1,192}?)'),
2129
				),
2130
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
2131
				'after' => '</blockquote>',
2132
				'trim' => 'both',
2133
				'block_level' => true,
2134
			),
2135
			// Legacy (alias of [color=red])
2136
			array(
2137
				'tag' => 'red',
2138
				'before' => '<span style="color: red;" class="bbc_color">',
2139
				'after' => '</span>',
2140
			),
2141
			array(
2142
				'tag' => 'right',
2143
				'before' => '<div class="righttext">',
2144
				'after' => '</div>',
2145
				'block_level' => true,
2146
			),
2147
			array(
2148
				'tag' => 'rtl',
2149
				'before' => '<bdo dir="rtl">',
2150
				'after' => '</bdo>',
2151
				'block_level' => true,
2152
			),
2153
			array(
2154
				'tag' => 's',
2155
				'before' => '<s>',
2156
				'after' => '</s>',
2157
			),
2158
			// Legacy (never a good idea)
2159
			array(
2160
				'tag' => 'shadow',
2161
				'type' => 'unparsed_commas',
2162
				'test' => '[#0-9a-zA-Z\-]{3,12},(left|right|top|bottom|[0123]\d{0,2})\]',
2163
				'before' => '<span style="text-shadow: $1 $2">',
2164
				'after' => '</span>',
2165
				'validate' => function(&$tag, &$data, $disabled)
2166
				{
2167
2168
					if ($data[1] == 'top' || (is_numeric($data[1]) && $data[1] < 50))
2169
						$data[1] = '0 -2px 1px';
2170
2171
					elseif ($data[1] == 'right' || (is_numeric($data[1]) && $data[1] < 100))
2172
						$data[1] = '2px 0 1px';
2173
2174
					elseif ($data[1] == 'bottom' || (is_numeric($data[1]) && $data[1] < 190))
2175
						$data[1] = '0 2px 1px';
2176
2177
					elseif ($data[1] == 'left' || (is_numeric($data[1]) && $data[1] < 280))
2178
						$data[1] = '-2px 0 1px';
2179
2180
					else
2181
						$data[1] = '1px 1px 1px';
2182
				},
2183
			),
2184
			array(
2185
				'tag' => 'size',
2186
				'type' => 'unparsed_equals',
2187
				'test' => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)\]',
2188
				'before' => '<span style="font-size: $1;" class="bbc_size">',
2189
				'after' => '</span>',
2190
			),
2191
			array(
2192
				'tag' => 'size',
2193
				'type' => 'unparsed_equals',
2194
				'test' => '[1-7]\]',
2195
				'before' => '<span style="font-size: $1;" class="bbc_size">',
2196
				'after' => '</span>',
2197
				'validate' => function(&$tag, &$data, $disabled)
2198
				{
2199
					$sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95);
2200
					$data = $sizes[$data] . 'em';
2201
				},
2202
			),
2203
			array(
2204
				'tag' => 'sub',
2205
				'before' => '<sub>',
2206
				'after' => '</sub>',
2207
			),
2208
			array(
2209
				'tag' => 'sup',
2210
				'before' => '<sup>',
2211
				'after' => '</sup>',
2212
			),
2213
			array(
2214
				'tag' => 'table',
2215
				'before' => '<table class="bbc_table">',
2216
				'after' => '</table>',
2217
				'trim' => 'inside',
2218
				'require_children' => array('tr'),
2219
				'block_level' => true,
2220
			),
2221
			array(
2222
				'tag' => 'td',
2223
				'before' => '<td>',
2224
				'after' => '</td>',
2225
				'require_parents' => array('tr'),
2226
				'trim' => 'outside',
2227
				'block_level' => true,
2228
				'disabled_before' => '',
2229
				'disabled_after' => '',
2230
			),
2231
			array(
2232
				'tag' => 'time',
2233
				'type' => 'unparsed_content',
2234
				'content' => '$1',
2235
				'validate' => function(&$tag, &$data, $disabled)
2236
				{
2237
					if (is_numeric($data))
2238
						$data = timeformat($data);
2239
2240
					$tag['content'] = '<span class="bbc_time">$1</span>';
2241
				},
2242
			),
2243
			array(
2244
				'tag' => 'tr',
2245
				'before' => '<tr>',
2246
				'after' => '</tr>',
2247
				'require_parents' => array('table'),
2248
				'require_children' => array('td'),
2249
				'trim' => 'both',
2250
				'block_level' => true,
2251
				'disabled_before' => '',
2252
				'disabled_after' => '',
2253
			),
2254
			// Legacy (the <tt> element is dead)
2255
			array(
2256
				'tag' => 'tt',
2257
				'before' => '<span class="monospace">',
2258
				'after' => '</span>',
2259
			),
2260
			array(
2261
				'tag' => 'u',
2262
				'before' => '<u>',
2263
				'after' => '</u>',
2264
			),
2265
			array(
2266
				'tag' => 'url',
2267
				'type' => 'unparsed_content',
2268
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
2269
				'validate' => function(&$tag, &$data, $disabled)
2270
				{
2271
					$data = normalize_iri(strtr($data, array('<br>' => '')));
2272
2273
					$scheme = parse_iri($data, PHP_URL_SCHEME);
2274
					if (empty($scheme))
2275
						$data = '//' . ltrim($data, ':/');
2276
2277
					$ascii_url = iri_to_url($data);
2278
					if ($ascii_url !== $data)
2279
						$tag['content'] = str_replace('href="$1"', 'href="' . $ascii_url . '"', $tag['content']);
2280
				},
2281
			),
2282
			array(
2283
				'tag' => 'url',
2284
				'type' => 'unparsed_equals',
2285
				'quoted' => 'optional',
2286
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
2287
				'after' => '</a>',
2288
				'validate' => function(&$tag, &$data, $disabled)
2289
				{
2290
					$data = iri_to_url($data);
2291
2292
					$scheme = parse_iri($data, PHP_URL_SCHEME);
2293
					if (empty($scheme))
2294
						$data = '//' . ltrim($data, ':/');
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type boolean; however, parameter $string of ltrim() 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

2294
						$data = '//' . ltrim(/** @scrutinizer ignore-type */ $data, ':/');
Loading history...
2295
				},
2296
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
2297
				'disabled_after' => ' ($1)',
2298
			),
2299
			// Legacy (alias of [color=white])
2300
			array(
2301
				'tag' => 'white',
2302
				'before' => '<span style="color: white;" class="bbc_color">',
2303
				'after' => '</span>',
2304
			),
2305
			array(
2306
				'tag' => 'youtube',
2307
				'type' => 'unparsed_content',
2308
				'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>',
2309
				'disabled_content' => '<a href="https://www.youtube.com/watch?v=$1" target="_blank" rel="noopener">https://www.youtube.com/watch?v=$1</a>',
2310
				'block_level' => true,
2311
			),
2312
		);
2313
2314
		// Inside these tags autolink is not recommendable.
2315
		$no_autolink_tags = array(
2316
			'url',
2317
			'iurl',
2318
			'email',
2319
			'img',
2320
			'html',
2321
		);
2322
2323
		// Let mods add new BBC without hassle.
2324
		call_integration_hook('integrate_bbc_codes', array(&$codes, &$no_autolink_tags));
2325
2326
		// This is mainly for the bbc manager, so it's easy to add tags above.  Custom BBC should be added above this line.
2327
		if ($message === false)
0 ignored issues
show
introduced by
The condition $message === false is always false.
Loading history...
2328
		{
2329
			usort(
2330
				$codes,
2331
				function($a, $b)
2332
				{
2333
					return strcmp($a['tag'], $b['tag']);
2334
				}
2335
			);
2336
			return $codes;
2337
		}
2338
2339
		// So the parser won't skip them.
2340
		$itemcodes = array(
2341
			'*' => 'disc',
2342
			'@' => 'disc',
2343
			'+' => 'square',
2344
			'x' => 'square',
2345
			'#' => 'square',
2346
			'o' => 'circle',
2347
			'O' => 'circle',
2348
			'0' => 'circle',
2349
		);
2350
		if (!isset($disabled['li']) && !isset($disabled['list']))
2351
		{
2352
			foreach ($itemcodes as $c => $dummy)
2353
				$bbc_codes[$c] = array();
2354
		}
2355
2356
		// Shhhh!
2357
		if (!isset($disabled['color']))
2358
		{
2359
			$codes[] = array(
2360
				'tag' => 'chrissy',
2361
				'before' => '<span style="color: #cc0099;">',
2362
				'after' => ' :-*</span>',
2363
			);
2364
			$codes[] = array(
2365
				'tag' => 'kissy',
2366
				'before' => '<span style="color: #cc0099;">',
2367
				'after' => ' :-*</span>',
2368
			);
2369
		}
2370
		$codes[] = array(
2371
			'tag' => 'cowsay',
2372
			'parameters' => array(
2373
				'e' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => 'oo', 'validate' => function ($eyes) use ($smcFunc)
2374
					{
2375
						return $smcFunc['substr']($eyes . 'oo', 0, 2);
2376
					},
2377
				),
2378
				't' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => '  ', 'validate' => function ($tongue) use ($smcFunc)
2379
					{
2380
						return $smcFunc['substr']($tongue . '  ', 0, 2);
2381
					},
2382
				),
2383
			),
2384
			'before' => '<pre data-e="{e}" data-t="{t}"><div>',
2385
			'after' => '</div></pre>',
2386
			'block_level' => true,
2387
			'validate' => function(&$tag, &$data, $disabled, $params)
2388
			{
2389
				static $moo = true;
2390
2391
				if ($moo)
2392
				{
2393
					addInlineJavaScript("\n\t" . base64_decode(
2394
						'aWYoZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImJvdmluZV9vcmFjbGU
2395
						iKT09PW51bGwpe2xldCBzdHlsZU5vZGU9ZG9jdW1lbnQuY3JlYXRlRWx
2396
						lbWVudCgic3R5bGUiKTtzdHlsZU5vZGUuaWQ9ImJvdmluZV9vcmFjbGU
2397
						iO3N0eWxlTm9kZS5pbm5lckhUTUw9J3ByZVtkYXRhLWVdW2RhdGEtdF1
2398
						7d2hpdGUtc3BhY2U6cHJlLXdyYXA7bGluZS1oZWlnaHQ6aW5pdGlhbDt
2399
						9cHJlW2RhdGEtZV1bZGF0YS10XSA+IGRpdntkaXNwbGF5OnRhYmxlO2J
2400
						vcmRlcjoxcHggc29saWQ7Ym9yZGVyLXJhZGl1czowLjVlbTtwYWRkaW5
2401
						nOjFjaDttYXgtd2lkdGg6ODBjaDttaW4td2lkdGg6MTJjaDt9cHJlW2R
2402
						hdGEtZV1bZGF0YS10XTo6YWZ0ZXJ7ZGlzcGxheTppbmxpbmUtYmxvY2s
2403
						7bWFyZ2luLWxlZnQ6OGNoO21pbi13aWR0aDoyMGNoO2RpcmVjdGlvbjp
2404
						sdHI7Y29udGVudDpcJ1xcNUMgXCdcJyBcJ1wnIF5fX15cXEEgXCdcJyB
2405
						cXDVDIFwnXCcgKFwnIGF0dHIoZGF0YS1lKSBcJylcXDVDX19fX19fX1x
2406
						cQSBcJ1wnIFwnXCcgXCdcJyAoX18pXFw1QyBcJ1wnIFwnXCcgXCdcJyB
2407
						cJ1wnIFwnXCcgXCdcJyBcJ1wnIClcXDVDL1xcNUNcXEEgXCdcJyBcJ1w
2408
						nIFwnXCcgXCdcJyBcJyBhdHRyKGRhdGEtdCkgXCcgfHwtLS0tdyB8XFx
2409
						BIFwnXCcgXCdcJyBcJ1wnIFwnXCcgXCdcJyBcJ1wnIFwnXCcgfHwgXCd
2410
						cJyBcJ1wnIFwnXCcgXCdcJyB8fFwnO30nO2RvY3VtZW50LmdldEVsZW1
2411
						lbnRzQnlUYWdOYW1lKCJoZWFkIilbMF0uYXBwZW5kQ2hpbGQoc3R5bGV
2412
						Ob2RlKTt9'
2413
					), true);
2414
2415
					$moo = false;
2416
				}
2417
			}
2418
		);
2419
2420
		foreach ($codes as $code)
2421
		{
2422
			// Make it easier to process parameters later
2423
			if (!empty($code['parameters']))
2424
				ksort($code['parameters'], SORT_STRING);
2425
2426
			// If we are not doing every tag only do ones we are interested in.
2427
			if (empty($parse_tags) || in_array($code['tag'], $parse_tags))
2428
				$bbc_codes[substr($code['tag'], 0, 1)][] = $code;
2429
		}
2430
		$codes = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $codes is dead and can be removed.
Loading history...
2431
	}
2432
2433
	// Shall we take the time to cache this?
2434
	if ($cache_id != '' && !empty($cache_enable) && (($cache_enable >= 2 && isset($message[1000])) || isset($message[2400])) && empty($parse_tags))
2435
	{
2436
		// It's likely this will change if the message is modified.
2437
		$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']);
2438
2439
		if (($temp = cache_get_data($cache_key, 240)) != null)
2440
			return $temp;
2441
2442
		$cache_t = microtime(true);
2443
	}
2444
2445
	if ($smileys === 'print')
0 ignored issues
show
introduced by
The condition $smileys === 'print' is always false.
Loading history...
2446
	{
2447
		// [glow], [shadow], and [move] can't really be printed.
2448
		$disabled['glow'] = true;
2449
		$disabled['shadow'] = true;
2450
		$disabled['move'] = true;
2451
2452
		// Colors can't well be displayed... supposed to be black and white.
2453
		$disabled['color'] = true;
2454
		$disabled['black'] = true;
2455
		$disabled['blue'] = true;
2456
		$disabled['white'] = true;
2457
		$disabled['red'] = true;
2458
		$disabled['green'] = true;
2459
		$disabled['me'] = true;
2460
2461
		// Color coding doesn't make sense.
2462
		$disabled['php'] = true;
2463
2464
		// Links are useless on paper... just show the link.
2465
		$disabled['ftp'] = true;
2466
		$disabled['url'] = true;
2467
		$disabled['iurl'] = true;
2468
		$disabled['email'] = true;
2469
		$disabled['flash'] = true;
2470
2471
		// @todo Change maybe?
2472
		if (!isset($_GET['images']))
2473
		{
2474
			$disabled['img'] = true;
2475
			$disabled['attach'] = true;
2476
		}
2477
2478
		// Maybe some custom BBC need to be disabled for printing.
2479
		call_integration_hook('integrate_bbc_print', array(&$disabled));
2480
	}
2481
2482
	$open_tags = array();
2483
	$message = strtr($message, array("\n" => '<br>'));
2484
2485
	if (!empty($parse_tags))
2486
	{
2487
		$real_alltags_regex = $alltags_regex;
2488
		$alltags_regex = '';
2489
	}
2490
	if (empty($alltags_regex))
2491
	{
2492
		$alltags = array();
2493
		foreach ($bbc_codes as $section)
2494
		{
2495
			foreach ($section as $code)
2496
				$alltags[] = $code['tag'];
2497
		}
2498
		$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_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

2498
		$alltags_regex = '(?' . '>\b' . /** @scrutinizer ignore-type */ build_regex(array_unique($alltags)) . '\b|' . build_regex(array_keys($itemcodes)) . ')';
Loading history...
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

2498
		$alltags_regex = '(?' . '>\b' . build_regex(array_unique($alltags)) . '\b|' . /** @scrutinizer ignore-type */ build_regex(array_keys($itemcodes)) . ')';
Loading history...
2499
	}
2500
2501
	$pos = -1;
2502
	while ($pos !== false)
2503
	{
2504
		$last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos;
2505
		preg_match('~\[/?(?=' . $alltags_regex . ')~i', $message, $matches, PREG_OFFSET_CAPTURE, $pos + 1);
2506
		$pos = isset($matches[0][1]) ? $matches[0][1] : false;
2507
2508
		// Failsafe.
2509
		if ($pos === false || $last_pos > $pos)
2510
			$pos = strlen($message) + 1;
2511
2512
		// Can't have a one letter smiley, URL, or email! (Sorry.)
2513
		if ($last_pos < $pos - 1)
2514
		{
2515
			// Make sure the $last_pos is not negative.
2516
			$last_pos = max($last_pos, 0);
2517
2518
			// Pick a block of data to do some raw fixing on.
2519
			$data = substr($message, $last_pos, $pos - $last_pos);
2520
2521
			$placeholders = array();
2522
			$placeholders_counter = 0;
2523
			// Wrap in "private use" Unicode characters to ensure there will be no conflicts.
2524
			$placeholder_template = html_entity_decode('&#xE03C;') . '%1$s' . html_entity_decode('&#xE03E;');
2525
2526
			// Take care of some HTML!
2527
			if (!empty($modSettings['enablePostHTML']) && strpos($data, '&lt;') !== false)
2528
			{
2529
				$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);
2530
2531
				// <br> should be empty.
2532
				$empty_tags = array('br', 'hr');
2533
				foreach ($empty_tags as $tag)
2534
					$data = str_replace(array('&lt;' . $tag . '&gt;', '&lt;' . $tag . '/&gt;', '&lt;' . $tag . ' /&gt;'), '<' . $tag . '>', $data);
2535
2536
				// b, u, i, s, pre... basic tags.
2537
				$closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote', 'strong');
2538
				foreach ($closable_tags as $tag)
2539
				{
2540
					$diff = substr_count($data, '&lt;' . $tag . '&gt;') - substr_count($data, '&lt;/' . $tag . '&gt;');
2541
					$data = strtr($data, array('&lt;' . $tag . '&gt;' => '<' . $tag . '>', '&lt;/' . $tag . '&gt;' => '</' . $tag . '>'));
2542
2543
					if ($diff > 0)
2544
						$data = substr($data, 0, -1) . str_repeat('</' . $tag . '>', $diff) . substr($data, -1);
2545
				}
2546
2547
				// Do <img ...> - with security... action= -> action-.
2548
				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);
2549
				if (!empty($matches[0]))
2550
				{
2551
					$replaces = array();
2552
					foreach ($matches[2] as $match => $imgtag)
2553
					{
2554
						$alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^&quot;|&quot;$~', '', $matches[3][$match]);
2555
2556
						// Remove action= from the URL - no funny business, now.
2557
						// @todo Testing this preg_match seems pointless
2558
						if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) != 0)
2559
							$imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag);
2560
2561
						$placeholder = sprintf($placeholder_template, ++$placeholders_counter);
2562
						$placeholders[$placeholder] = '[img' . $alt . ']' . $imgtag . '[/img]';
2563
2564
						$replaces[$matches[0][$match]] = $placeholder;
2565
					}
2566
2567
					$data = strtr($data, $replaces);
2568
				}
2569
			}
2570
2571
			if (!empty($modSettings['autoLinkUrls']))
2572
			{
2573
				// Are we inside tags that should be auto linked?
2574
				$no_autolink_area = false;
2575
				if (!empty($open_tags))
2576
				{
2577
					foreach ($open_tags as $open_tag)
2578
						if (in_array($open_tag['tag'], $no_autolink_tags))
2579
							$no_autolink_area = true;
2580
				}
2581
2582
				// Don't go backwards.
2583
				// @todo Don't think is the real solution....
2584
				$lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0;
2585
				if ($pos < $lastAutoPos)
2586
					$no_autolink_area = true;
2587
				$lastAutoPos = $pos;
2588
2589
				if (!$no_autolink_area)
2590
				{
2591
					// An &nbsp; right after a URL can break the autolinker
2592
					if (strpos($data, '&nbsp;') !== false)
2593
					{
2594
						$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

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

2732
							$pcre_subroutines['bracket_quote'] = '[' . $bracket_quote_chars . ']|&' . /** @scrutinizer ignore-type */ build_regex($bracket_quote_entities, '~');
Loading history...
2733
							$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

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

2942
								$url_regex .= '(?<' . $name . '>' . /** @scrutinizer ignore-type */ $subroutine . ')';
Loading history...
2943
2944
							$url_regex .= ')';
2945
						}
2946
2947
						$tmp_data = preg_replace_callback(
2948
							'~' . $url_regex . '~i' . ($context['utf8'] ? 'u' : ''),
2949
							function($matches) use ($schemes)
2950
							{
2951
								$url = array_shift($matches);
2952
2953
								// If this isn't a clean URL, bail out
2954
								if ($url != sanitize_iri($url))
2955
									return $url;
2956
2957
								// Ensure the host name is in its canonical form.
2958
								$url = normalize_iri($url);
2959
2960
								$parsedurl = parse_iri($url);
2961
2962
								if (!isset($parsedurl['scheme']))
2963
									$parsedurl['scheme'] = '';
2964
2965
								if ($parsedurl['scheme'] == 'mailto')
2966
								{
2967
									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...
2968
										return $url;
2969
2970
									// Is this version of PHP capable of validating this email address?
2971
									$can_validate = defined('FILTER_FLAG_EMAIL_UNICODE') || strlen($parsedurl['path']) == strspn(strtolower($parsedurl['path']), 'abcdefghijklmnopqrstuvwxyz0123456789!#$%&\'*+-/=?^_`{|}~.@');
2972
2973
									$flags = defined('FILTER_FLAG_EMAIL_UNICODE') ? FILTER_FLAG_EMAIL_UNICODE : null;
2974
2975
									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

2975
									if (!$can_validate || filter_var($parsedurl['path'], FILTER_VALIDATE_EMAIL, /** @scrutinizer ignore-type */ $flags) !== false)
Loading history...
2976
										return '[email=' . str_replace('mailto:', '', $url) . ']' . $url . '[/email]';
2977
									else
2978
										return $url;
2979
								}
2980
2981
								// Are we linking a schemeless URL or naked domain name (e.g. "example.com")?
2982
								if (empty($parsedurl['scheme']))
2983
									$fullUrl = '//' . ltrim($url, ':/');
2984
								else
2985
									$fullUrl = $url;
2986
2987
								// Make sure that $fullUrl really is valid
2988
								if (in_array($parsedurl['scheme'], $schemes['forbidden']) || (!in_array($parsedurl['scheme'], $schemes['no_authority']) && validate_iri((strpos($fullUrl, '//') === 0 ? 'http:' : '') . $fullUrl) === false))
2989
									return $url;
2990
2991
								return '[url=&quot;' . str_replace(array('[', ']'), array('&#91;', '&#93;'), iri_to_url($fullUrl)) . '&quot;]' . $url . '[/url]';
2992
							},
2993
							$data
2994
						);
2995
2996
						if (!is_null($tmp_data))
2997
							$data = $tmp_data;
2998
					}
2999
3000
					// Next, emails...  Must be careful not to step on enablePostHTML logic above...
3001
					if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false && stripos($data, 'mailto:') === false)
3002
					{
3003
						// Preceded by a space or start of line
3004
						$email_regex = '(?<=^|<br>|[\h\v])' .
3005
3006
						// An email address
3007
						'[' . $domain_label_chars . '_.]{1,80}' .
3008
						'@' .
3009
						'[' . $domain_label_chars . '.]+' .
3010
						'\.' . $modSettings['tld_regex'] .
3011
3012
						// Followed by a non-domain character or end of line
3013
						'(?=[^' . $domain_label_chars . ']|$)';
3014
3015
						$tmp_data = preg_replace('~' . $email_regex . '~i' . ($context['utf8'] ? 'u' : ''), '[email]$0[/email]', $data);
3016
3017
						if (!is_null($tmp_data))
3018
							$data = $tmp_data;
3019
					}
3020
3021
					// Save a little memory.
3022
					unset($tmp_data);
3023
				}
3024
			}
3025
3026
			// Restore any placeholders
3027
			$data = strtr($data, $placeholders);
3028
3029
			$data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));
3030
3031
			// If it wasn't changed, no copying or other boring stuff has to happen!
3032
			if ($data != substr($message, $last_pos, $pos - $last_pos))
3033
			{
3034
				$message = substr($message, 0, $last_pos) . $data . substr($message, $pos);
3035
3036
				// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
3037
				$old_pos = strlen($data) + $last_pos;
3038
				$pos = strpos($message, '[', $last_pos);
3039
				$pos = $pos === false ? $old_pos : min($pos, $old_pos);
3040
			}
3041
		}
3042
3043
		// Are we there yet?  Are we there yet?
3044
		if ($pos >= strlen($message) - 1)
3045
			break;
3046
3047
		$tag_character = strtolower($message[$pos + 1]);
3048
3049
		if ($tag_character == '/' && !empty($open_tags))
3050
		{
3051
			$pos2 = strpos($message, ']', $pos + 1);
3052
			if ($pos2 == $pos + 2)
3053
				continue;
3054
3055
			$look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));
3056
3057
			// A closing tag that doesn't match any open tags? Skip it.
3058
			if (!in_array($look_for, array_map(function($code) { return $code['tag']; }, $open_tags)))
3059
				continue;
3060
3061
			$to_close = array();
3062
			$block_level = null;
3063
3064
			do
3065
			{
3066
				$tag = array_pop($open_tags);
3067
				if (!$tag)
3068
					break;
3069
3070
				if (!empty($tag['block_level']))
3071
				{
3072
					// Only find out if we need to.
3073
					if ($block_level === false)
3074
					{
3075
						array_push($open_tags, $tag);
3076
						break;
3077
					}
3078
3079
					// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
3080
					if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]]))
3081
					{
3082
						foreach ($bbc_codes[$look_for[0]] as $temp)
3083
							if ($temp['tag'] == $look_for)
3084
							{
3085
								$block_level = !empty($temp['block_level']);
3086
								break;
3087
							}
3088
					}
3089
3090
					if ($block_level !== true)
3091
					{
3092
						$block_level = false;
3093
						array_push($open_tags, $tag);
3094
						break;
3095
					}
3096
				}
3097
3098
				$to_close[] = $tag;
3099
			}
3100
			while ($tag['tag'] != $look_for);
3101
3102
			// Did we just eat through everything and not find it?
3103
			if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
3104
			{
3105
				$open_tags = $to_close;
3106
				continue;
3107
			}
3108
			elseif (!empty($to_close) && $tag['tag'] != $look_for)
3109
			{
3110
				if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]]))
3111
				{
3112
					foreach ($bbc_codes[$look_for[0]] as $temp)
3113
						if ($temp['tag'] == $look_for)
3114
						{
3115
							$block_level = !empty($temp['block_level']);
3116
							break;
3117
						}
3118
				}
3119
3120
				// We're not looking for a block level tag (or maybe even a tag that exists...)
3121
				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...
3122
				{
3123
					foreach ($to_close as $tag)
3124
						array_push($open_tags, $tag);
3125
					continue;
3126
				}
3127
			}
3128
3129
			foreach ($to_close as $tag)
3130
			{
3131
				$message = substr($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1);
3132
				$pos += strlen($tag['after']) + 2;
3133
				$pos2 = $pos - 1;
3134
3135
				// See the comment at the end of the big loop - just eating whitespace ;).
3136
				$whitespace_regex = '';
3137
				if (!empty($tag['block_level']))
3138
					$whitespace_regex .= '(&nbsp;|\s)*(<br\s*/?' . '>)?';
3139
				// Trim one line of whitespace after unnested tags, but all of it after nested ones
3140
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
3141
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
3142
3143
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
3144
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
3145
			}
3146
3147
			if (!empty($to_close))
3148
			{
3149
				$to_close = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $to_close is dead and can be removed.
Loading history...
3150
				$pos--;
3151
			}
3152
3153
			continue;
3154
		}
3155
3156
		// No tags for this character, so just keep going (fastest possible course.)
3157
		if (!isset($bbc_codes[$tag_character]))
3158
			continue;
3159
3160
		$inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
3161
		$tag = null;
3162
		foreach ($bbc_codes[$tag_character] as $possible)
3163
		{
3164
			$pt_strlen = strlen($possible['tag']);
3165
3166
			// Not a match?
3167
			if (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag'])
3168
				continue;
3169
3170
			$next_c = isset($message[$pos + 1 + $pt_strlen]) ? $message[$pos + 1 + $pt_strlen] : '';
3171
3172
			// A tag is the last char maybe
3173
			if ($next_c == '')
3174
				break;
3175
3176
			// A test validation?
3177
			if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0)
3178
				continue;
3179
			// Do we want parameters?
3180
			elseif (!empty($possible['parameters']))
3181
			{
3182
				// Are all the parameters optional?
3183
				$param_required = false;
3184
				foreach ($possible['parameters'] as $param)
3185
				{
3186
					if (empty($param['optional']))
3187
					{
3188
						$param_required = true;
3189
						break;
3190
					}
3191
				}
3192
3193
				if ($param_required && $next_c != ' ')
3194
					continue;
3195
			}
3196
			elseif (isset($possible['type']))
3197
			{
3198
				// Do we need an equal sign?
3199
				if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=')
3200
					continue;
3201
				// Maybe we just want a /...
3202
				if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + $pt_strlen, 2) != '/]' && substr($message, $pos + 1 + $pt_strlen, 3) != ' /]')
3203
					continue;
3204
				// An immediate ]?
3205
				if ($possible['type'] == 'unparsed_content' && $next_c != ']')
3206
					continue;
3207
			}
3208
			// No type means 'parsed_content', which demands an immediate ] without parameters!
3209
			elseif ($next_c != ']')
3210
				continue;
3211
3212
			// Check allowed tree?
3213
			if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
3214
				continue;
3215
			elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
3216
				continue;
3217
			// If this is in the list of disallowed child tags, don't parse it.
3218
			elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
3219
				continue;
3220
3221
			$pos1 = $pos + 1 + $pt_strlen + 1;
3222
3223
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
3224
			if ($possible['tag'] == 'quote')
3225
			{
3226
				// Start with standard
3227
				$quote_alt = false;
3228
				foreach ($open_tags as $open_quote)
3229
				{
3230
					// Every parent quote this quote has flips the styling
3231
					if ($open_quote['tag'] == 'quote')
3232
						$quote_alt = !$quote_alt;
0 ignored issues
show
introduced by
The condition $quote_alt is always false.
Loading history...
3233
				}
3234
				// Add a class to the quote to style alternating blockquotes
3235
				$possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">'));
3236
			}
3237
3238
			// This is long, but it makes things much easier and cleaner.
3239
			if (!empty($possible['parameters']))
3240
			{
3241
				// Build a regular expression for each parameter for the current tag.
3242
				$regex_key = $smcFunc['json_encode']($possible['parameters']);
3243
				if (!isset($params_regexes[$regex_key]))
3244
				{
3245
					$params_regexes[$regex_key] = '';
3246
3247
					foreach ($possible['parameters'] as $p => $info)
3248
						$params_regexes[$regex_key] .= '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . '\s*)' . (empty($info['optional']) ? '' : '?');
3249
				}
3250
3251
				// Extract the string that potentially holds our parameters.
3252
				$blob = preg_split('~\[/?(?:' . $alltags_regex . ')~i', substr($message, $pos));
3253
				$blobs = preg_split('~\]~i', $blob[1]);
3254
3255
				$splitters = implode('=|', array_keys($possible['parameters'])) . '=';
3256
3257
				// Progressively append more blobs until we find our parameters or run out of blobs
3258
				$blob_counter = 1;
3259
				while ($blob_counter <= count($blobs))
3260
				{
3261
					$given_param_string = implode(']', array_slice($blobs, 0, $blob_counter++));
3262
3263
					$given_params = preg_split('~\s(?=(' . $splitters . '))~i', $given_param_string);
3264
					sort($given_params, SORT_STRING);
3265
3266
					$match = preg_match('~^' . $params_regexes[$regex_key] . '$~i', implode(' ', $given_params), $matches) !== 0;
3267
3268
					if ($match)
3269
						break;
3270
				}
3271
3272
				// Didn't match our parameter list, try the next possible.
3273
				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...
3274
					continue;
3275
3276
				$params = array();
3277
				for ($i = 1, $n = count($matches); $i < $n; $i += 2)
3278
				{
3279
					$key = strtok(ltrim($matches[$i]), '=');
3280
					if ($key === false)
3281
						continue;
3282
					elseif (isset($possible['parameters'][$key]['value']))
3283
						$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
3284
					elseif (isset($possible['parameters'][$key]['validate']))
3285
						$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
3286
					else
3287
						$params['{' . $key . '}'] = $matches[$i + 1];
3288
3289
					// Just to make sure: replace any $ or { so they can't interpolate wrongly.
3290
					$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
3291
				}
3292
3293
				foreach ($possible['parameters'] as $p => $info)
3294
				{
3295
					if (!isset($params['{' . $p . '}']))
3296
					{
3297
						if (!isset($info['default']))
3298
							$params['{' . $p . '}'] = '';
3299
						elseif (isset($possible['parameters'][$p]['value']))
3300
							$params['{' . $p . '}'] = strtr($possible['parameters'][$p]['value'], array('$1' => $info['default']));
3301
						elseif (isset($possible['parameters'][$p]['validate']))
3302
							$params['{' . $p . '}'] = $possible['parameters'][$p]['validate']($info['default']);
3303
						else
3304
							$params['{' . $p . '}'] = $info['default'];
3305
					}
3306
				}
3307
3308
				$tag = $possible;
3309
3310
				// Put the parameters into the string.
3311
				if (isset($tag['before']))
3312
					$tag['before'] = strtr($tag['before'], $params);
3313
				if (isset($tag['after']))
3314
					$tag['after'] = strtr($tag['after'], $params);
3315
				if (isset($tag['content']))
3316
					$tag['content'] = strtr($tag['content'], $params);
3317
3318
				$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...
3319
			}
3320
			else
3321
			{
3322
				$tag = $possible;
3323
				$params = array();
3324
			}
3325
			break;
3326
		}
3327
3328
		// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
3329
		if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
3330
		{
3331
			if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
3332
				continue;
3333
3334
			$tag = $itemcodes[$message[$pos + 1]];
3335
3336
			// First let's set up the tree: it needs to be in a list, or after an li.
3337
			if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
3338
			{
3339
				$open_tags[] = array(
3340
					'tag' => 'list',
3341
					'after' => '</ul>',
3342
					'block_level' => true,
3343
					'require_children' => array('li'),
3344
					'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
3345
				);
3346
				$code = '<ul class="bbc_list">';
3347
			}
3348
			// We're in a list item already: another itemcode?  Close it first.
3349
			elseif ($inside['tag'] == 'li')
3350
			{
3351
				array_pop($open_tags);
3352
				$code = '</li>';
3353
			}
3354
			else
3355
				$code = '';
3356
3357
			// Now we open a new tag.
3358
			$open_tags[] = array(
3359
				'tag' => 'li',
3360
				'after' => '</li>',
3361
				'trim' => 'outside',
3362
				'block_level' => true,
3363
				'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
3364
			);
3365
3366
			// First, open the tag...
3367
			$code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
3368
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
3369
			$pos += strlen($code) - 1 + 2;
3370
3371
			// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
3372
			$pos2 = strpos($message, '<br>', $pos);
3373
			$pos3 = strpos($message, '[/', $pos);
3374
			if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
3375
			{
3376
				preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 4), $matches);
3377
				$message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
3378
3379
				$open_tags[count($open_tags) - 2]['after'] = '</ul>';
3380
			}
3381
			// Tell the [list] that it needs to close specially.
3382
			else
3383
			{
3384
				// Move the li over, because we're not sure what we'll hit.
3385
				$open_tags[count($open_tags) - 1]['after'] = '';
3386
				$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
3387
			}
3388
3389
			continue;
3390
		}
3391
3392
		// Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
3393
		if ($tag === null && $inside !== null && !empty($inside['require_children']))
3394
		{
3395
			array_pop($open_tags);
3396
3397
			$message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
3398
			$pos += strlen($inside['after']) - 1 + 2;
3399
		}
3400
3401
		// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
3402
		if ($tag === null)
3403
			continue;
3404
3405
		// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
3406
		if (isset($inside['disallow_children']))
3407
			$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
3408
3409
		// Is this tag disabled?
3410
		if (isset($disabled[$tag['tag']]))
3411
		{
3412
			if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
3413
			{
3414
				$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
3415
				$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
3416
				$tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
3417
			}
3418
			elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
3419
			{
3420
				$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
3421
				$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
3422
			}
3423
			else
3424
				$tag['content'] = $tag['disabled_content'];
3425
		}
3426
3427
		// we use this a lot
3428
		$tag_strlen = strlen($tag['tag']);
3429
3430
		// The only special case is 'html', which doesn't need to close things.
3431
		if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
3432
		{
3433
			$n = count($open_tags) - 1;
3434
			while (empty($open_tags[$n]['block_level']) && $n >= 0)
3435
				$n--;
3436
3437
			// Close all the non block level tags so this tag isn't surrounded by them.
3438
			for ($i = count($open_tags) - 1; $i > $n; $i--)
3439
			{
3440
				$message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
3441
				$ot_strlen = strlen($open_tags[$i]['after']);
3442
				$pos += $ot_strlen + 2;
3443
				$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...
3444
3445
				// Trim or eat trailing stuff... see comment at the end of the big loop.
3446
				$whitespace_regex = '';
3447
				if (!empty($tag['block_level']))
3448
					$whitespace_regex .= '(&nbsp;|\s)*(<br>)?';
3449
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
3450
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
3451
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
3452
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
3453
3454
				array_pop($open_tags);
3455
			}
3456
		}
3457
3458
		// Can't read past the end of the message
3459
		$pos1 = min(strlen($message), $pos1);
3460
3461
		// No type means 'parsed_content'.
3462
		if (!isset($tag['type']))
3463
		{
3464
			$open_tags[] = $tag;
3465
3466
			// There's no data to change, but maybe do something based on params?
3467
			$data = null;
3468
			if (isset($tag['validate']))
3469
				$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...
3470
3471
			$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
3472
			$pos += strlen($tag['before']) - 1 + 2;
3473
		}
3474
		// Don't parse the content, just skip it.
3475
		elseif ($tag['type'] == 'unparsed_content')
3476
		{
3477
			$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
3478
			if ($pos2 === false)
3479
				continue;
3480
3481
			$data = substr($message, $pos1, $pos2 - $pos1);
3482
3483
			if (!empty($tag['block_level']) && substr($data, 0, 4) == '<br>')
3484
				$data = substr($data, 4);
3485
3486
			if (isset($tag['validate']))
3487
				$tag['validate']($tag, $data, $disabled, $params);
3488
3489
			$code = strtr($tag['content'], array('$1' => $data));
3490
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
3491
3492
			$pos += strlen($code) - 1 + 2;
3493
			$last_pos = $pos + 1;
3494
		}
3495
		// Don't parse the content, just skip it.
3496
		elseif ($tag['type'] == 'unparsed_equals_content')
3497
		{
3498
			// The value may be quoted for some tags - check.
3499
			if (isset($tag['quoted']))
3500
			{
3501
				$quoted = substr($message, $pos1, 6) == '&quot;';
3502
				if ($tag['quoted'] != 'optional' && !$quoted)
3503
					continue;
3504
3505
				if ($quoted)
3506
					$pos1 += 6;
3507
			}
3508
			else
3509
				$quoted = false;
3510
3511
			$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...
3512
			if ($pos2 === false)
3513
				continue;
3514
3515
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3516
			if ($pos3 === false)
3517
				continue;
3518
3519
			$data = array(
3520
				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...
3521
				substr($message, $pos1, $pos2 - $pos1)
3522
			);
3523
3524
			if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '<br>')
3525
				$data[0] = substr($data[0], 4);
3526
3527
			// Validation for my parking, please!
3528
			if (isset($tag['validate']))
3529
				$tag['validate']($tag, $data, $disabled, $params);
3530
3531
			$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
3532
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3533
			$pos += strlen($code) - 1 + 2;
3534
		}
3535
		// A closed tag, with no content or value.
3536
		elseif ($tag['type'] == 'closed')
3537
		{
3538
			$pos2 = strpos($message, ']', $pos);
3539
3540
			// Maybe a custom BBC wants to do something special?
3541
			$data = null;
3542
			if (isset($tag['validate']))
3543
				$tag['validate']($tag, $data, $disabled, $params);
3544
3545
			$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
3546
			$pos += strlen($tag['content']) - 1 + 2;
3547
		}
3548
		// This one is sorta ugly... :/.  Unfortunately, it's needed for flash.
3549
		elseif ($tag['type'] == 'unparsed_commas_content')
3550
		{
3551
			$pos2 = strpos($message, ']', $pos1);
3552
			if ($pos2 === false)
3553
				continue;
3554
3555
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3556
			if ($pos3 === false)
3557
				continue;
3558
3559
			// We want $1 to be the content, and the rest to be csv.
3560
			$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
3561
			$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
3562
3563
			if (isset($tag['validate']))
3564
				$tag['validate']($tag, $data, $disabled, $params);
3565
3566
			$code = $tag['content'];
3567
			foreach ($data as $k => $d)
3568
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3569
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3570
			$pos += strlen($code) - 1 + 2;
3571
		}
3572
		// This has parsed content, and a csv value which is unparsed.
3573
		elseif ($tag['type'] == 'unparsed_commas')
3574
		{
3575
			$pos2 = strpos($message, ']', $pos1);
3576
			if ($pos2 === false)
3577
				continue;
3578
3579
			$data = explode(',', substr($message, $pos1, $pos2 - $pos1));
3580
3581
			if (isset($tag['validate']))
3582
				$tag['validate']($tag, $data, $disabled, $params);
3583
3584
			// Fix after, for disabled code mainly.
3585
			foreach ($data as $k => $d)
3586
				$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
3587
3588
			$open_tags[] = $tag;
3589
3590
			// Replace them out, $1, $2, $3, $4, etc.
3591
			$code = $tag['before'];
3592
			foreach ($data as $k => $d)
3593
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3594
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1);
3595
			$pos += strlen($code) - 1 + 2;
3596
		}
3597
		// A tag set to a value, parsed or not.
3598
		elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
3599
		{
3600
			// The value may be quoted for some tags - check.
3601
			if (isset($tag['quoted']))
3602
			{
3603
				$quoted = substr($message, $pos1, 6) == '&quot;';
3604
				if ($tag['quoted'] != 'optional' && !$quoted)
3605
					continue;
3606
3607
				if ($quoted)
3608
					$pos1 += 6;
3609
			}
3610
			else
3611
				$quoted = false;
3612
3613
			if ($quoted)
3614
			{
3615
				$end_of_value = strpos($message, '&quot;]', $pos1);
3616
				$nested_tag = strpos($message, '=&quot;', $pos1);
3617
				// Check so this is not just an quoted url ending with a =
3618
				if ($nested_tag && substr($message, $nested_tag, 8) == '=&quot;]')
3619
					$nested_tag = false;
3620
				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...
3621
					// Nested tag with quoted value detected, use next end tag
3622
					$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...
3623
			}
3624
3625
			$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...
3626
			if ($pos2 === false)
3627
				continue;
3628
3629
			$data = substr($message, $pos1, $pos2 - $pos1);
3630
3631
			// Validation for my parking, please!
3632
			if (isset($tag['validate']))
3633
				$tag['validate']($tag, $data, $disabled, $params);
3634
3635
			// For parsed content, we must recurse to avoid security problems.
3636
			if ($tag['type'] != 'unparsed_equals')
3637
				$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
3638
3639
			$tag['after'] = strtr($tag['after'], array('$1' => $data));
3640
3641
			$open_tags[] = $tag;
3642
3643
			$code = strtr($tag['before'], array('$1' => $data));
3644
			$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...
3645
			$pos += strlen($code) - 1 + 2;
3646
		}
3647
3648
		// If this is block level, eat any breaks after it.
3649
		if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '<br>')
3650
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 5);
3651
3652
		// Are we trimming outside this tag?
3653
		if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
3654
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
3655
	}
3656
3657
	// Close any remaining tags.
3658
	while ($tag = array_pop($open_tags))
3659
		$message .= "\n" . $tag['after'] . "\n";
3660
3661
	// Parse the smileys within the parts where it can be done safely.
3662
	if ($smileys === true)
3663
	{
3664
		$message_parts = explode("\n", $message);
3665
		for ($i = 0, $n = count($message_parts); $i < $n; $i += 2)
3666
			parsesmileys($message_parts[$i]);
3667
3668
		$message = implode('', $message_parts);
3669
	}
3670
3671
	// No smileys, just get rid of the markers.
3672
	else
3673
		$message = strtr($message, array("\n" => ''));
3674
3675
	if ($message !== '' && $message[0] === ' ')
3676
		$message = '&nbsp;' . substr($message, 1);
3677
3678
	// Cleanup whitespace.
3679
	$message = strtr($message, array('  ' => ' &nbsp;', "\r" => '', "\n" => '<br>', '<br> ' => '<br>&nbsp;', '&#13;' => "\n"));
3680
3681
	// Allow mods access to what parse_bbc created
3682
	call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
3683
3684
	// Cache the output if it took some time...
3685
	if (isset($cache_key, $cache_t) && microtime(true) - $cache_t > 0.05)
3686
		cache_put_data($cache_key, $message, 240);
3687
3688
	// If this was a force parse revert if needed.
3689
	if (!empty($parse_tags))
3690
	{
3691
		$alltags_regex = empty($real_alltags_regex) ? '' : $real_alltags_regex;
3692
		unset($real_alltags_regex);
3693
	}
3694
	elseif (!empty($bbc_codes))
3695
		$bbc_lang_locales[$txt['lang_locale']] = $bbc_codes;
3696
3697
	return $message;
3698
}
3699
3700
/**
3701
 * Parse smileys in the passed message.
3702
 *
3703
 * The smiley parsing function which makes pretty faces appear :).
3704
 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
3705
 * These are specifically not parsed in code tags [url=mailto:[email protected]]
3706
 * Caches the smileys from the database or array in memory.
3707
 * Doesn't return anything, but rather modifies message directly.
3708
 *
3709
 * @param string &$message The message to parse smileys in
3710
 */
3711
function parsesmileys(&$message)
3712
{
3713
	global $modSettings, $txt, $user_info, $context, $smcFunc;
3714
	static $smileyPregSearch = null, $smileyPregReplacements = array();
3715
3716
	// No smiley set at all?!
3717
	if ($user_info['smiley_set'] == 'none' || trim($message) == '')
3718
		return;
3719
3720
	// Maybe a mod wants to implement an alternative method (e.g. emojis instead of images)
3721
	call_integration_hook('integrate_smileys', array(&$smileyPregSearch, &$smileyPregReplacements));
3722
3723
	// If smileyPregSearch hasn't been set, do it now.
3724
	if (empty($smileyPregSearch))
3725
	{
3726
		// Cache for longer when customized smiley codes aren't enabled
3727
		$cache_time = empty($modSettings['smiley_enable']) ? 7200 : 480;
3728
3729
		// Load the smileys in reverse order by length so they don't get parsed incorrectly.
3730
		if (($temp = cache_get_data('parsing_smileys_' . $user_info['smiley_set'], $cache_time)) == null)
3731
		{
3732
			$result = $smcFunc['db_query']('', '
3733
				SELECT s.code, f.filename, s.description
3734
				FROM {db_prefix}smileys AS s
3735
					JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
3736
				WHERE f.smiley_set = {string:smiley_set}' . (empty($modSettings['smiley_enable']) ? '
3737
					AND s.code IN ({array_string:default_codes})' : '') . '
3738
				ORDER BY LENGTH(s.code) DESC',
3739
				array(
3740
					'default_codes' => array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'),
3741
					'smiley_set' => $user_info['smiley_set'],
3742
				)
3743
			);
3744
			$smileysfrom = array();
3745
			$smileysto = array();
3746
			$smileysdescs = array();
3747
			while ($row = $smcFunc['db_fetch_assoc']($result))
3748
			{
3749
				$smileysfrom[] = $row['code'];
3750
				$smileysto[] = $smcFunc['htmlspecialchars']($row['filename']);
3751
				$smileysdescs[] = !empty($txt['icon_' . strtolower($row['description'])]) ? $txt['icon_' . strtolower($row['description'])] : $row['description'];
3752
			}
3753
			$smcFunc['db_free_result']($result);
3754
3755
			cache_put_data('parsing_smileys_' . $user_info['smiley_set'], array($smileysfrom, $smileysto, $smileysdescs), $cache_time);
3756
		}
3757
		else
3758
			list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
3759
3760
		// The non-breaking-space is a complex thing...
3761
		$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
3762
3763
		// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:[email protected]] doesn't parse the :D smiley)
3764
		$smileyPregReplacements = array();
3765
		$searchParts = array();
3766
		$smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
3767
3768
		for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
3769
		{
3770
			$specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES);
3771
			$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">';
3772
3773
			$smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
3774
3775
			$searchParts[] = $smileysfrom[$i];
3776
			if ($smileysfrom[$i] != $specialChars)
3777
			{
3778
				$smileyPregReplacements[$specialChars] = $smileyCode;
3779
				$searchParts[] = $specialChars;
3780
3781
				// Some 2.0 hex htmlchars are in there as 3 digits; allow for finding leading 0 or not
3782
				$specialChars2 = preg_replace('/&#(\d{2});/', '&#0$1;', $specialChars);
3783
				if ($specialChars2 != $specialChars)
3784
				{
3785
					$smileyPregReplacements[$specialChars2] = $smileyCode;
3786
					$searchParts[] = $specialChars2;
3787
				}
3788
			}
3789
		}
3790
3791
		$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

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

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

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

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

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

5088
		if (strpos(/** @scrutinizer ignore-type */ $test, 'not found') !== false)
Loading history...
5089
			$host = '';
5090
		// Invalid server option?
5091
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
5092
			updateSettings(array('host_to_dis' => 1));
5093
		// Maybe it found something, after all?
5094
		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

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

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

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

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

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

6134
			$temp_file = tempnam($cachedir, md5(/** @scrutinizer ignore-type */ $data));
Loading history...
6135
			file_put_contents($temp_file, $data);
6136
			$is_path = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $is_path is dead and can be removed.
Loading history...
6137
			$data = $temp_file;
6138
		}
6139
6140
		$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

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

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

6291
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
6292
6293
	// remove left to right / right to left overrides
6294
	if ($num === 0x202D || $num === 0x202E)
6295
		return '';
6296
6297
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
6298
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
6299
		return '&#' . $num . ';';
6300
6301
	if (empty($context['utf8']))
6302
	{
6303
		// no control characters
6304
		if ($num < 0x20)
6305
			return '';
6306
		// text is text
6307
		elseif ($num < 0x80)
6308
			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

6308
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
6309
		// all others get html-ised
6310
		else
6311
			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

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

6349
	$num = $matches[1][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[1], 1)) : (int) $matches[1];
Loading history...
6350
6351
	// <0x20 are control characters, > 0x10FFFF is past the end of the utf8 character set
6352
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text), 0x202D-E are left to right overrides
6353
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202D || $num === 0x202E)
6354
		return '';
6355
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
6356
	elseif ($num < 0x80)
6357
		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

6357
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
6358
	// <0x800 (2048)
6359
	elseif ($num < 0x800)
6360
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
6361
	// < 0x10000 (65536)
6362
	elseif ($num < 0x10000)
6363
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6364
	// <= 0x10FFFF (1114111)
6365
	else
6366
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6367
}
6368
6369
/**
6370
 * Strips out invalid html entities, replaces others with html style &#123; codes
6371
 *
6372
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
6373
 * strpos, strlen, substr etc
6374
 *
6375
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
6376
 * @return string The fixed string
6377
 */
6378
function entity_fix__callback($matches)
6379
{
6380
	if (!isset($matches[2]))
6381
		return '';
6382
6383
	$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

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

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

6584
	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...
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

6584
	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...
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

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

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

7211
		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

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

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

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

7723
	if (filter_var($url, FILTER_VALIDATE_URL, /** @scrutinizer ignore-type */ $flags) !== false)
Loading history...
7724
		return $iri;
7725
	else
7726
		return false;
7727
}
7728
7729
/**
7730
 * A wrapper for `filter_var($url, FILTER_SANITIZE_URL)` that can handle URLs
7731
 * with international characters (a.k.a. IRIs)
7732
 *
7733
 * Note: The returned value will still be an IRI, not a URL. To convert to URL,
7734
 * feed the result of this function to iri_to_url()
7735
 *
7736
 * @param string $iri The IRI to sanitize.
7737
 * @return string|bool The sanitized version of the IRI
7738
 */
7739
function sanitize_iri($iri)
7740
{
7741
	// Encode any non-ASCII characters (but not space or control characters of any sort)
7742
	// Also encode '%' in order to preserve anything that is already percent-encoded.
7743
	$iri = preg_replace_callback(
7744
		'~[^\x00-\x7F\pZ\pC]|%~u',
7745
		function($matches)
7746
		{
7747
			return rawurlencode($matches[0]);
7748
		},
7749
		$iri
7750
	);
7751
7752
	// Perform normal sanitization
7753
	$iri = filter_var($iri, FILTER_SANITIZE_URL);
7754
7755
	// Decode the non-ASCII characters
7756
	$iri = rawurldecode($iri);
7757
7758
	return $iri;
7759
}
7760
7761
/**
7762
 * Performs Unicode normalization on IRIs.
7763
 *
7764
 * Internally calls sanitize_iri(), then performs Unicode normalization on the
7765
 * IRI as a whole, using NFKC normalization for the domain name (see RFC 3491)
7766
 * and NFC normalization for the rest.
7767
 *
7768
 * @param string $iri The IRI to normalize.
7769
 * @return string|bool The normalized version of the IRI.
7770
 */
7771
function normalize_iri($iri)
7772
{
7773
	global $sourcedir, $context, $txt, $db_character_set;
7774
7775
	// If we are not using UTF-8, just sanitize and return.
7776
	if (isset($context['utf8']) ? !$context['utf8'] : (isset($txt['lang_character_set']) ? $txt['lang_character_set'] != 'UTF-8' : (isset($db_character_set) && $db_character_set != 'utf8')))
7777
		return sanitize_iri($iri);
7778
7779
	require_once($sourcedir . '/Subs-Charset.php');
7780
7781
	$iri = sanitize_iri(utf8_normalize_c($iri));
7782
7783
	$host = parse_iri((strpos($iri, '//') === 0 ? 'http:' : '') . $iri, PHP_URL_HOST);
7784
7785
	if (!empty($host))
7786
	{
7787
		$normalized_host = utf8_normalize_kc_casefold($host);
7788
		$pos = strpos($iri, $host);
7789
	}
7790
	else
7791
	{
7792
		$normalized_host = '';
7793
		$pos = 0;
7794
	}
7795
7796
	$before_host = substr($iri, 0, $pos);
7797
	$after_host = substr($iri, $pos + strlen($host));
7798
7799
	return $before_host . $normalized_host . $after_host;
7800
}
7801
7802
/**
7803
 * Converts a URL with international characters (an IRI) into a pure ASCII URL
7804
 *
7805
 * Uses Punycode to encode any non-ASCII characters in the domain name, and uses
7806
 * standard URL encoding on the rest.
7807
 *
7808
 * @param string $iri A IRI that may or may not contain non-ASCII characters.
7809
 * @return string|bool The URL version of the IRI.
7810
 */
7811
function iri_to_url($iri)
7812
{
7813
	global $sourcedir, $context, $txt, $db_character_set;
7814
7815
	// Sanity check: must be using UTF-8 to do this.
7816
	if (isset($context['utf8']) ? !$context['utf8'] : (isset($txt['lang_character_set']) ? $txt['lang_character_set'] != 'UTF-8' : (isset($db_character_set) && $db_character_set != 'utf8')))
7817
		return $iri;
7818
7819
	require_once($sourcedir . '/Subs-Charset.php');
7820
7821
	$iri = sanitize_iri(utf8_normalize_c($iri));
7822
7823
	$host = parse_iri((strpos($iri, '//') === 0 ? 'http:' : '') . $iri, PHP_URL_HOST);
7824
7825
	if (!empty($host))
7826
	{
7827
		if (!function_exists('idn_to_ascii'))
7828
			require_once($sourcedir . '/Subs-Compat.php');
7829
7830
		// Convert the host using the Punycode algorithm
7831
		$encoded_host = idn_to_ascii($host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
7832
7833
		$pos = strpos($iri, $host);
7834
	}
7835
	else
7836
	{
7837
		$encoded_host = '';
7838
		$pos = 0;
7839
	}
7840
7841
	$before_host = substr($iri, 0, $pos);
7842
	$after_host = substr($iri, $pos + strlen($host));
7843
7844
	// Encode any disallowed characters in the rest of the URL
7845
	$unescaped = array(
7846
		'%21' => '!', '%23' => '#', '%24' => '$', '%26' => '&',
7847
		'%27' => "'", '%28' => '(', '%29' => ')', '%2A' => '*',
7848
		'%2B' => '+', '%2C' => ',', '%2F' => '/', '%3A' => ':',
7849
		'%3B' => ';', '%3D' => '=', '%3F' => '?', '%40' => '@',
7850
		'%25' => '%',
7851
	);
7852
7853
	$before_host = strtr(rawurlencode($before_host), $unescaped);
7854
	$after_host = strtr(rawurlencode($after_host), $unescaped);
7855
7856
	return $before_host . $encoded_host . $after_host;
7857
}
7858
7859
/**
7860
 * Decodes a URL containing encoded international characters to UTF-8
7861
 *
7862
 * Decodes any Punycode encoded characters in the domain name, then uses
7863
 * standard URL decoding on the rest.
7864
 *
7865
 * @param string $url The pure ASCII version of a URL.
7866
 * @return string|bool The UTF-8 version of the URL.
7867
 */
7868
function url_to_iri($url)
7869
{
7870
	global $sourcedir, $context, $txt, $db_character_set;
7871
7872
	// Sanity check: must be using UTF-8 to do this.
7873
	if (isset($context['utf8']) ? !$context['utf8'] : (isset($txt['lang_character_set']) ? $txt['lang_character_set'] != 'UTF-8' : (isset($db_character_set) && $db_character_set != 'utf8')))
7874
		return $url;
7875
7876
	$host = parse_iri((strpos($url, '//') === 0 ? 'http:' : '') . $url, PHP_URL_HOST);
7877
7878
	if (!empty($host))
7879
	{
7880
		if (!function_exists('idn_to_utf8'))
7881
			require_once($sourcedir . '/Subs-Compat.php');
7882
7883
		// Decode the domain from Punycode
7884
		$decoded_host = idn_to_utf8($host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
7885
7886
		$pos = strpos($url, $host);
7887
	}
7888
	else
7889
	{
7890
		$decoded_host = '';
7891
		$pos = 0;
7892
	}
7893
7894
	$before_host = substr($url, 0, $pos);
7895
	$after_host = substr($url, $pos + strlen($host));
7896
7897
	// Decode the rest of the URL, but preserve escaped URL syntax characters.
7898
	$double_escaped = array(
7899
		'%21' => '%2521', '%23' => '%2523', '%24' => '%2524', '%26' => '%2526',
7900
		'%27' => '%2527', '%28' => '%2528', '%29' => '%2529', '%2A' => '%252A',
7901
		'%2B' => '%252B', '%2C' => '%252C', '%2F' => '%252F', '%3A' => '%253A',
7902
		'%3B' => '%253B', '%3D' => '%253D', '%3F' => '%253F', '%40' => '%2540',
7903
		'%25' => '%2525',
7904
	);
7905
7906
	$before_host = rawurldecode(strtr($before_host, $double_escaped));
7907
	$after_host = rawurldecode(strtr($after_host, $double_escaped));
7908
7909
	return $before_host . $decoded_host . $after_host;
7910
}
7911
7912
/**
7913
 * Ensures SMF's scheduled tasks are being run as intended
7914
 *
7915
 * If the admin activated the cron_is_real_cron setting, but the cron job is
7916
 * not running things at least once per day, we need to go back to SMF's default
7917
 * behaviour using "web cron" JavaScript calls.
7918
 */
7919
function check_cron()
7920
{
7921
	global $modSettings, $smcFunc, $txt;
7922
7923
	if (!empty($modSettings['cron_is_real_cron']) && time() - @intval($modSettings['cron_last_checked']) > 84600)
7924
	{
7925
		$request = $smcFunc['db_query']('', '
7926
			SELECT COUNT(*)
7927
			FROM {db_prefix}scheduled_tasks
7928
			WHERE disabled = {int:not_disabled}
7929
				AND next_time < {int:yesterday}',
7930
			array(
7931
				'not_disabled' => 0,
7932
				'yesterday' => time() - 84600,
7933
			)
7934
		);
7935
		list($overdue) = $smcFunc['db_fetch_row']($request);
7936
		$smcFunc['db_free_result']($request);
7937
7938
		// If we have tasks more than a day overdue, cron isn't doing its job.
7939
		if (!empty($overdue))
7940
		{
7941
			loadLanguage('ManageScheduledTasks');
7942
			log_error($txt['cron_not_working']);
7943
			updateSettings(array('cron_is_real_cron' => 0));
7944
		}
7945
		else
7946
			updateSettings(array('cron_last_checked' => time()));
7947
	}
7948
}
7949
7950
/**
7951
 * Sends an appropriate HTTP status header based on a given status code
7952
 *
7953
 * @param int $code The status code
7954
 * @param string $status The string for the status. Set automatically if not provided.
7955
 */
7956
function send_http_status($code, $status = '')
7957
{
7958
	global $sourcedir;
7959
7960
	$statuses = array(
7961
		204 => 'No Content',
7962
		206 => 'Partial Content',
7963
		304 => 'Not Modified',
7964
		400 => 'Bad Request',
7965
		403 => 'Forbidden',
7966
		404 => 'Not Found',
7967
		410 => 'Gone',
7968
		500 => 'Internal Server Error',
7969
		503 => 'Service Unavailable',
7970
	);
7971
7972
	$protocol = preg_match('~^\s*(HTTP/[12]\.\d)\s*$~i', $_SERVER['SERVER_PROTOCOL'], $matches) ? $matches[1] : 'HTTP/1.0';
7973
7974
	// Typically during these requests, we have cleaned the response (ob_*clean), ensure these headers exist.
7975
	require_once($sourcedir . '/Security.php');
7976
	frameOptionsHeader();
7977
	corsPolicyHeader();
7978
7979
	if (!isset($statuses[$code]) && empty($status))
7980
		header($protocol . ' 500 Internal Server Error');
7981
	else
7982
		header($protocol . ' ' . $code . ' ' . (!empty($status) ? $status : $statuses[$code]));
7983
}
7984
7985
/**
7986
 * Concatenates an array of strings into a grammatically correct sentence list
7987
 *
7988
 * Uses formats defined in the language files to build the list appropropriately
7989
 * for the currently loaded language.
7990
 *
7991
 * @param array $list An array of strings to concatenate.
7992
 * @return string The localized sentence list.
7993
 */
7994
function sentence_list($list)
7995
{
7996
	global $txt;
7997
7998
	// Make sure the bare necessities are defined
7999
	if (empty($txt['sentence_list_format']['n']))
8000
		$txt['sentence_list_format']['n'] = '{series}';
8001
	if (!isset($txt['sentence_list_separator']))
8002
		$txt['sentence_list_separator'] = ', ';
8003
	if (!isset($txt['sentence_list_separator_alt']))
8004
		$txt['sentence_list_separator_alt'] = '; ';
8005
8006
	// Which format should we use?
8007
	if (isset($txt['sentence_list_format'][count($list)]))
8008
		$format = $txt['sentence_list_format'][count($list)];
8009
	else
8010
		$format = $txt['sentence_list_format']['n'];
8011
8012
	// Do we want the normal separator or the alternate?
8013
	$separator = $txt['sentence_list_separator'];
8014
	foreach ($list as $item)
8015
	{
8016
		if (strpos($item, $separator) !== false)
8017
		{
8018
			$separator = $txt['sentence_list_separator_alt'];
8019
			$format = strtr($format, trim($txt['sentence_list_separator']), trim($separator));
8020
			break;
8021
		}
8022
	}
8023
8024
	$replacements = array();
8025
8026
	// Special handling for the last items on the list
8027
	$i = 0;
8028
	while (empty($done))
8029
	{
8030
		if (strpos($format, '{'. --$i . '}') !== false)
8031
			$replacements['{'. $i . '}'] = array_pop($list);
8032
		else
8033
			$done = true;
8034
	}
8035
	unset($done);
8036
8037
	// Special handling for the first items on the list
8038
	$i = 0;
8039
	while (empty($done))
8040
	{
8041
		if (strpos($format, '{'. ++$i . '}') !== false)
8042
			$replacements['{'. $i . '}'] = array_shift($list);
8043
		else
8044
			$done = true;
8045
	}
8046
	unset($done);
8047
8048
	// Whatever is left
8049
	$replacements['{series}'] = implode($separator, $list);
8050
8051
	// Do the deed
8052
	return strtr($format, $replacements);
8053
}
8054
8055
/**
8056
 * Truncate an array to a specified length
8057
 *
8058
 * @param array $array The array to truncate
8059
 * @param int $max_length The upperbound on the length
8060
 * @param int $deep How levels in an multidimensional array should the function take into account.
8061
 * @return array The truncated array
8062
 */
8063
function truncate_array($array, $max_length = 1900, $deep = 3)
8064
{
8065
	$array = (array) $array;
8066
8067
	$curr_length = array_length($array, $deep);
8068
8069
	if ($curr_length <= $max_length)
8070
		return $array;
8071
8072
	else
8073
	{
8074
		// Truncate each element's value to a reasonable length
8075
		$param_max = floor($max_length / count($array));
8076
8077
		$current_deep = $deep - 1;
8078
8079
		foreach ($array as $key => &$value)
8080
		{
8081
			if (is_array($value))
8082
				if ($current_deep > 0)
8083
					$value = truncate_array($value, $current_deep);
8084
8085
			else
8086
				$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

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

8086
				$value = substr($value, 0, /** @scrutinizer ignore-type */ $param_max - strlen($key) - 5);
Loading history...
8087
		}
8088
8089
		return $array;
8090
	}
8091
}
8092
8093
/**
8094
 * array_length Recursive
8095
 * @param array $array
8096
 * @param int $deep How many levels should the function
8097
 * @return int
8098
 */
8099
function array_length($array, $deep = 3)
8100
{
8101
	// Work with arrays
8102
	$array = (array) $array;
8103
	$length = 0;
8104
8105
	$deep_count = $deep - 1;
8106
8107
	foreach ($array as $value)
8108
	{
8109
		// Recursive?
8110
		if (is_array($value))
8111
		{
8112
			// No can't do
8113
			if ($deep_count <= 0)
8114
				continue;
8115
8116
			$length += array_length($value, $deep_count);
8117
		}
8118
		else
8119
			$length += strlen($value);
8120
	}
8121
8122
	return $length;
8123
}
8124
8125
/**
8126
 * Compares existance request variables against an array.
8127
 *
8128
 * The input array is associative, where keys denote accepted values
8129
 * in a request variable denoted by `$req_val`. Values can be:
8130
 *
8131
 * - another associative array where at least one key must be found
8132
 *   in the request and their values are accepted request values.
8133
 * - A scalar value, in which case no furthur checks are done.
8134
 *
8135
 * @param array $array
8136
 * @param string $req_var request variable
8137
 *
8138
 * @return bool whether any of the criteria was satisfied
8139
 */
8140
function is_filtered_request(array $array, $req_var)
8141
{
8142
	$matched = false;
8143
	if (isset($_REQUEST[$req_var], $array[$_REQUEST[$req_var]]))
8144
	{
8145
		if (is_array($array[$_REQUEST[$req_var]]))
8146
		{
8147
			foreach ($array[$_REQUEST[$req_var]] as $subtype => $subnames)
8148
				$matched |= isset($_REQUEST[$subtype]) && in_array($_REQUEST[$subtype], $subnames);
8149
		}
8150
		else
8151
			$matched = true;
8152
	}
8153
8154
	return (bool) $matched;
8155
}
8156
8157
/**
8158
 * Clean up the XML to make sure it doesn't contain invalid characters.
8159
 *
8160
 * See https://www.w3.org/TR/xml/#charsets
8161
 *
8162
 * @param string $string The string to clean
8163
 * @return string The cleaned string
8164
 */
8165
function cleanXml($string)
8166
{
8167
	global $context;
8168
8169
	$illegal_chars = array(
8170
		// Remove all ASCII control characters except \t, \n, and \r.
8171
		"\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08",
8172
		"\x0B", "\x0C", "\x0E", "\x0F", "\x10", "\x11", "\x12", "\x13", "\x14",
8173
		"\x15", "\x16", "\x17", "\x18", "\x19", "\x1A", "\x1B", "\x1C", "\x1D",
8174
		"\x1E", "\x1F",
8175
		// Remove \xFFFE and \xFFFF
8176
		"\xEF\xBF\xBE", "\xEF\xBF\xBF",
8177
	);
8178
8179
	$string = str_replace($illegal_chars, '', $string);
8180
8181
	// The Unicode surrogate pair code points should never be present in our
8182
	// strings to begin with, but if any snuck in, they need to be removed.
8183
	if (!empty($context['utf8']) && strpos($string, "\xED") !== false)
8184
		$string = preg_replace('/\xED[\xA0-\xBF][\x80-\xBF]/', '', $string);
8185
8186
	return $string;
8187
}
8188
8189
/**
8190
 * Escapes (replaces) characters in strings to make them safe for use in javascript
8191
 *
8192
 * @param string $string The string to escape
8193
 * @return string The escaped string
8194
 */
8195
function JavaScriptEscape($string)
8196
{
8197
	global $scripturl;
8198
8199
	return '\'' . strtr($string, array(
8200
		"\r" => '',
8201
		"\n" => '\\n',
8202
		"\t" => '\\t',
8203
		'\\' => '\\\\',
8204
		'\'' => '\\\'',
8205
		'</' => '<\' + \'/',
8206
		'<script' => '<scri\'+\'pt',
8207
		'<body>' => '<bo\'+\'dy>',
8208
		'<a href' => '<a hr\'+\'ef',
8209
		$scripturl => '\' + smf_scripturl + \'',
8210
	)) . '\'';
8211
}
8212
8213
function tokenTxtReplace($stringSubject = '')
8214
{
8215
	global $txt;
8216
8217
	if (empty($stringSubject))
8218
		return '';
8219
8220
	$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...
8221
	$toFind = array();
8222
	$replaceWith = array();
8223
8224
	if (!empty($matches[1]))
8225
		foreach ($matches[1] as $token) {
8226
			$toFind[] = '{' . $token . '}';
8227
			$replaceWith[] = isset($txt[$token]) ? $txt[$token] : $token;
8228
		}
8229
8230
	return str_replace($toFind, $replaceWith, $stringSubject);
8231
}
8232
8233
?>