Issues (1065)

Sources/Subs.php (99 issues)

Code
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 2025 Simple Machines and individual contributors
11
 * @license https://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1.6
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
			'timezone',
318
			'time_offset',
319
			'avatar',
320
			'lngfile',
321
		);
322
		$vars_to_integrate = array_intersect($integration_vars, array_keys($data));
323
324
		// Only proceed if there are any variables left to call the integration function.
325
		if (count($vars_to_integrate) != 0)
326
		{
327
			// Fetch a list of member_names if necessary
328
			if ((!is_array($members) && $members === $user_info['id']) || (is_array($members) && count($members) == 1 && in_array($user_info['id'], $members)))
329
				$member_names = array($user_info['username']);
330
			else
331
			{
332
				$member_names = array();
333
				$request = $smcFunc['db_query']('', '
334
					SELECT member_name
335
					FROM {db_prefix}members
336
					WHERE ' . $condition,
337
					$parameters
338
				);
339
				while ($row = $smcFunc['db_fetch_assoc']($request))
340
					$member_names[] = $row['member_name'];
341
				$smcFunc['db_free_result']($request);
342
			}
343
344
			if (!empty($member_names))
345
				foreach ($vars_to_integrate as $var)
346
					call_integration_hook('integrate_change_member_data', array($member_names, $var, &$data[$var], &$knownInts, &$knownFloats));
347
		}
348
	}
349
350
	$setString = '';
351
	foreach ($data as $var => $val)
352
	{
353
		switch ($var)
354
		{
355
			case  'birthdate':
356
				$type = 'date';
357
				break;
358
359
			case 'member_ip':
360
			case 'member_ip2':
361
				$type = 'inet';
362
				break;
363
364
			default:
365
				$type = 'string';
366
		}
367
368
		if (in_array($var, $knownInts))
369
			$type = 'int';
370
371
		elseif (in_array($var, $knownFloats))
372
			$type = 'float';
373
374
		// Doing an increment?
375
		if ($var == 'alerts' && ($val === '+' || $val === '-'))
376
		{
377
			include_once($sourcedir . '/Profile-Modify.php');
378
			if (is_array($members))
379
			{
380
				$val = 'CASE ';
381
				foreach ($members as $k => $v)
382
					$val .= 'WHEN id_member = ' . $v . ' THEN '. alert_count($v, true) . ' ';
383
384
				$val = $val . ' END';
385
				$type = 'raw';
386
			}
387
388
			else
389
				$val = alert_count($members, true);
390
		}
391
392
		elseif ($type == 'int' && ($val === '+' || $val === '-'))
393
		{
394
			$val = $var . ' ' . $val . ' 1';
395
			$type = 'raw';
396
		}
397
398
		// Ensure posts, instant_messages, and unread_messages don't overflow or underflow.
399
		if (in_array($var, array('posts', 'instant_messages', 'unread_messages')))
400
		{
401
			if (preg_match('~^' . $var . ' (\+ |- |\+ -)([\d]+)~', $val, $match))
402
			{
403
				if ($match[1] != '+ ')
404
					$val = 'CASE WHEN ' . $var . ' <= ' . abs($match[2]) . ' THEN 0 ELSE ' . $val . ' END';
405
406
				$type = 'raw';
407
			}
408
		}
409
410
		$setString .= ' ' . $var . ' = {' . $type . ':p_' . $var . '},';
411
		$parameters['p_' . $var] = $val;
412
	}
413
414
	$smcFunc['db_query']('', '
415
		UPDATE {db_prefix}members
416
		SET' . substr($setString, 0, -1) . '
417
		WHERE ' . $condition,
418
		$parameters
419
	);
420
421
	updateStats('postgroups', $members, array_keys($data));
422
423
	// Clear any caching?
424
	if (!empty($cache_enable) && $cache_enable >= 2 && !empty($members))
425
	{
426
		if (!is_array($members))
427
			$members = array($members);
428
429
		foreach ($members as $member)
430
		{
431
			if ($cache_enable >= 3)
432
			{
433
				cache_put_data('member_data-profile-' . $member, null, 120);
434
				cache_put_data('member_data-normal-' . $member, null, 120);
435
				cache_put_data('member_data-minimal-' . $member, null, 120);
436
			}
437
			cache_put_data('user_settings-' . $member, null, 60);
438
		}
439
	}
440
}
441
442
/**
443
 * Updates the settings table as well as $modSettings... only does one at a time if $update is true.
444
 *
445
 * - updates both the settings table and $modSettings array.
446
 * - all of changeArray's indexes and values are assumed to have escaped apostrophes (')!
447
 * - if a variable is already set to what you want to change it to, that
448
 *   variable will be skipped over; it would be unnecessary to reset.
449
 * - When use_update is true, UPDATEs will be used instead of REPLACE.
450
 * - when use_update is true, the value can be true or false to increment
451
 *  or decrement it, respectively.
452
 *
453
 * @param array $changeArray An array of info about what we're changing in 'setting' => 'value' format
454
 * @param bool $update Whether to use an UPDATE query instead of a REPLACE query
455
 */
456
function updateSettings($changeArray, $update = false)
457
{
458
	global $modSettings, $smcFunc;
459
460
	if (empty($changeArray) || !is_array($changeArray))
461
		return;
462
463
	$toRemove = array();
464
465
	// Go check if there is any setting to be removed.
466
	foreach ($changeArray as $k => $v)
467
		if ($v === null)
468
		{
469
			// Found some, remove them from the original array and add them to ours.
470
			unset($changeArray[$k]);
471
			$toRemove[] = $k;
472
		}
473
474
	// Proceed with the deletion.
475
	if (!empty($toRemove))
476
		$smcFunc['db_query']('', '
477
			DELETE FROM {db_prefix}settings
478
			WHERE variable IN ({array_string:remove})',
479
			array(
480
				'remove' => $toRemove,
481
			)
482
		);
483
484
	// In some cases, this may be better and faster, but for large sets we don't want so many UPDATEs.
485
	if ($update)
486
	{
487
		foreach ($changeArray as $variable => $value)
488
		{
489
			$smcFunc['db_query']('', '
490
				UPDATE {db_prefix}settings
491
				SET value = {' . ($value === false || $value === true ? 'raw' : 'string') . ':value}
492
				WHERE variable = {string:variable}',
493
				array(
494
					'value' => $value === true ? 'value + 1' : ($value === false ? 'value - 1' : $value),
495
					'variable' => $variable,
496
				)
497
			);
498
			$modSettings[$variable] = $value === true ? $modSettings[$variable] + 1 : ($value === false ? $modSettings[$variable] - 1 : $value);
499
		}
500
501
		// Clean out the cache and make sure the cobwebs are gone too.
502
		cache_put_data('modSettings', null, 90);
503
504
		return;
505
	}
506
507
	$replaceArray = array();
508
	foreach ($changeArray as $variable => $value)
509
	{
510
		// Don't bother if it's already like that ;).
511
		if (isset($modSettings[$variable]) && $modSettings[$variable] == $value)
512
			continue;
513
		// If the variable isn't set, but would only be set to nothing'ness, then don't bother setting it.
514
		elseif (!isset($modSettings[$variable]) && empty($value))
515
			continue;
516
517
		$replaceArray[] = array($variable, $value);
518
519
		$modSettings[$variable] = $value;
520
	}
521
522
	if (empty($replaceArray))
523
		return;
524
525
	$smcFunc['db_insert']('replace',
526
		'{db_prefix}settings',
527
		array('variable' => 'string-255', 'value' => 'string-65534'),
528
		$replaceArray,
529
		array('variable')
530
	);
531
532
	// Kill the cache - it needs redoing now, but we won't bother ourselves with that here.
533
	cache_put_data('modSettings', null, 90);
534
}
535
536
/**
537
 * Constructs a page list.
538
 *
539
 * - builds the page list, e.g. 1 ... 6 7 [8] 9 10 ... 15.
540
 * - flexible_start causes it to use "url.page" instead of "url;start=page".
541
 * - very importantly, cleans up the start value passed, and forces it to
542
 *   be a multiple of num_per_page.
543
 * - checks that start is not more than max_value.
544
 * - base_url should be the URL without any start parameter on it.
545
 * - uses the compactTopicPagesEnable and compactTopicPagesContiguous
546
 *   settings to decide how to display the menu.
547
 *
548
 * an example is available near the function definition.
549
 * $pageindex = constructPageIndex($scripturl . '?board=' . $board, $_REQUEST['start'], $num_messages, $maxindex, true);
550
 *
551
 * @param string $base_url The basic URL to be used for each link.
552
 * @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.
553
 * @param int $max_value The total number of items you are paginating for.
554
 * @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.
555
 * @param bool $flexible_start Whether a ;start=x component should be introduced into the URL automatically (see above)
556
 * @param bool $show_prevnext Whether the Previous and Next links should be shown (should be on only when navigating the list)
557
 *
558
 * @return string The complete HTML of the page index that was requested, formatted by the template.
559
 */
560
function constructPageIndex($base_url, &$start, $max_value, $num_per_page, $flexible_start = false, $show_prevnext = true)
561
{
562
	global $modSettings, $context, $smcFunc, $settings, $txt;
563
564
	// Save whether $start was less than 0 or not.
565
	$start = (int) $start;
566
	$start_invalid = $start < 0;
567
568
	// $start must be within bounds and be a multiple of $num_per_page.
569
	$start = min(max(0, $start), $max_value);
570
	$start = $start - ($start % $num_per_page);
571
572
	if (!isset($context['current_page']))
573
		$context['current_page'] = $start / $num_per_page;
574
575
	// Define some default page index settings for compatibility with old themes.
576
	// !!! Should this be moved to loadTheme()?
577
	if (!isset($settings['page_index']))
578
		$settings['page_index'] = array(
579
			'extra_before' => '<span class="pages">' . $txt['pages'] . '</span>',
580
			'previous_page' => '<span class="main_icons previous_page"></span>',
581
			'current_page' => '<span class="current_page">%1$d</span> ',
582
			'page' => '<a class="nav_page" href="{URL}">%2$s</a> ',
583
			'expand_pages' => '<span class="expand_pages" onclick="expandPages(this, {LINK}, {FIRST_PAGE}, {LAST_PAGE}, {PER_PAGE});"> ... </span>',
584
			'next_page' => '<span class="main_icons next_page"></span>',
585
			'extra_after' => '',
586
		);
587
588
	$last_page_value = (int) (($max_value - 1) / $num_per_page) * $num_per_page;
589
	$base_link = strtr($settings['page_index']['page'], array('{URL}' => $flexible_start ? $base_url : strtr($base_url, array('%' => '%%')) . ';start=%1$d'));
590
	$pageindex = $settings['page_index']['extra_before'];
591
592
	// Show the "prev page" link. (>prev page< 1 ... 6 7 [8] 9 10 ... 15 next page)
593
	if ($start != 0 && !$start_invalid && $show_prevnext)
594
		$pageindex .= sprintf($base_link, $start - $num_per_page, $settings['page_index']['previous_page']);
595
596
	// Compact pages is off or on?
597
	if (empty($modSettings['compactTopicPagesEnable']))
598
	{
599
		// Show all the pages.
600
		$display_page = 1;
601
		for ($counter = 0; $counter < $max_value; $counter += $num_per_page)
602
			$pageindex .= $start == $counter && !$start_invalid ? sprintf($settings['page_index']['current_page'], $display_page++) : sprintf($base_link, $counter, $display_page++);
603
	}
604
	else
605
	{
606
		// If they didn't enter an odd value, pretend they did.
607
		$page_contiguous = (int) ($modSettings['compactTopicPagesContiguous'] - ($modSettings['compactTopicPagesContiguous'] % 2)) / 2;
608
609
		// Show the first page. (prev page >1< ... 6 7 [8] 9 10 ... 15)
610
		if ($start > $num_per_page * $page_contiguous)
611
			$pageindex .= sprintf($base_link, 0, '1');
612
613
		// Show the ... after the first page.  (prev page 1 >...< 6 7 [8] 9 10 ... 15 next page)
614
		if ($start > $num_per_page * ($page_contiguous + 1))
615
			$pageindex .= strtr($settings['page_index']['expand_pages'], array(
616
				'{LINK}' => JavaScriptEscape($smcFunc['htmlspecialchars']($base_link)),
617
				'{FIRST_PAGE}' => $num_per_page,
618
				'{LAST_PAGE}' => $start - $num_per_page * $page_contiguous,
619
				'{PER_PAGE}' => $num_per_page,
620
			));
621
622
		for ($nCont = -$page_contiguous; $nCont <= $page_contiguous; $nCont++)
623
		{
624
			$tmpStart = $start + $num_per_page * $nCont;
625
			if ($nCont == 0)
626
			{
627
				// Show the current page. (prev page 1 ... 6 7 >[8]< 9 10 ... 15 next page)
628
				if (!$start_invalid)
629
					$pageindex .= sprintf($settings['page_index']['current_page'], $start / $num_per_page + 1);
630
				else
631
					$pageindex .= sprintf($base_link, $start, $start / $num_per_page + 1);
632
			}
633
			// Show the pages before the current one. (prev page 1 ... >6 7< [8] 9 10 ... 15 next page)
634
			// ... or ...
635
			// Show the pages after the current one... (prev page 1 ... 6 7 [8] >9 10< ... 15 next page)
636
			elseif (($nCont < 0 && $start >= $num_per_page * -$nCont) || ($nCont > 0 && $tmpStart <= $last_page_value))
637
				$pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
638
		}
639
640
		// Show the '...' part near the end. (prev page 1 ... 6 7 [8] 9 10 >...< 15 next page)
641
		if ($start + $num_per_page * ($page_contiguous + 1) < $last_page_value)
642
			$pageindex .= strtr($settings['page_index']['expand_pages'], array(
643
				'{LINK}' => JavaScriptEscape($smcFunc['htmlspecialchars']($base_link)),
644
				'{FIRST_PAGE}' => $start + $num_per_page * ($page_contiguous + 1),
645
				'{LAST_PAGE}' => $last_page_value,
646
				'{PER_PAGE}' => $num_per_page,
647
			));
648
649
		// Show the last number in the list. (prev page 1 ... 6 7 [8] 9 10 ... >15<  next page)
650
		if ($start + $num_per_page * $page_contiguous < $last_page_value)
651
			$pageindex .= sprintf($base_link, $last_page_value, $last_page_value / $num_per_page + 1);
652
	}
653
654
	// Show the "next page" link. (prev page 1 ... 6 7 [8] 9 10 ... 15 >next page<)
655
	if ($start != $last_page_value && !$start_invalid && $show_prevnext)
656
		$pageindex .= sprintf($base_link, $start + $num_per_page, $settings['page_index']['next_page']);
657
658
	$pageindex .= $settings['page_index']['extra_after'];
659
660
	return $pageindex;
661
}
662
663
/**
664
 * - Formats a number.
665
 * - uses the format of number_format to decide how to format the number.
666
 *   for example, it might display "1 234,50".
667
 * - caches the formatting data from the setting for optimization.
668
 *
669
 * @param float $number A number
670
 * @param bool|int $override_decimal_count If set, will use the specified number of decimal places. Otherwise it's automatically determined
671
 * @return string A formatted number
672
 */
673
function comma_format($number, $override_decimal_count = false)
674
{
675
	global $txt;
676
	static $thousands_separator = null, $decimal_separator = null, $decimal_count = null;
677
678
	// Cache these values...
679
	if ($decimal_separator === null)
680
	{
681
		// Not set for whatever reason?
682
		if (empty($txt['number_format']) || preg_match('~^1([^\d]*)?234([^\d]*)(0*?)$~', $txt['number_format'], $matches) != 1)
683
			return $number;
684
685
		// Cache these each load...
686
		$thousands_separator = $matches[1];
687
		$decimal_separator = $matches[2];
688
		$decimal_count = strlen($matches[3]);
689
	}
690
691
	// Format the string with our friend, number_format.
692
	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
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

692
	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...
693
}
694
695
/**
696
 * Format a time to make it look purdy.
697
 *
698
 * - returns a pretty formatted version of time based on the user's format in $user_info['time_format'].
699
 * - applies all necessary time offsets to the timestamp, unless offset_type is set.
700
 * - if todayMod is set and show_today was not not specified or true, an
701
 *   alternate format string is used to show the date with something to show it is "today" or "yesterday".
702
 * - performs localization (more than just strftime would do alone.)
703
 *
704
 * @param int $log_time A timestamp
705
 * @param bool|string $show_today Whether to show "Today"/"Yesterday" or just a date.
706
 *     If a string is specified, that is used to temporarily override the date format.
707
 * @param null|string $tzid Time zone to use when generating the formatted string.
708
 *     If empty, the user's time zone will be used.
709
 *     If set to 'forum', the value of $modSettings['default_timezone'] will be used.
710
 *     If set to a valid time zone identifier, that will be used.
711
 *     Otherwise, the value of date_default_timezone_get() will be used.
712
 * @return string A formatted time string
713
 */
714
function timeformat($log_time, $show_today = true, $tzid = null)
715
{
716
	global $context, $user_info, $txt, $modSettings;
717
	static $today;
718
719
	$log_time = min(max($log_time, PHP_INT_MIN), PHP_INT_MAX);
720
721
	// Ensure required values are set
722
	$user_info['time_format'] = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %H:%M');
723
724
	// For backward compatibility, replace empty values with user's time zone
725
	// and replace 'forum' with forum's default time zone.
726
	$tzid = empty($tzid) ? getUserTimezone() : (($tzid === 'forum' || @timezone_open((string) $tzid) === false) ? $modSettings['default_timezone'] : (string) $tzid);
727
728
	// Today and Yesterday?
729
	$prefix = '';
730
	if ($modSettings['todayMod'] >= 1 && $show_today === true)
731
	{
732
		if (!isset($today[$tzid]))
733
			$today[$tzid] = date_format(date_create('today ' . $tzid), 'U');
734
735
		// Tomorrow? We don't support the future. ;)
736
		if ($log_time >= $today[$tzid] + 86400)
737
		{
738
			$prefix = '';
739
		}
740
		// Today.
741
		elseif ($log_time >= $today[$tzid])
742
		{
743
			$prefix = $txt['today'];
744
		}
745
		// Yesterday.
746
		elseif ($modSettings['todayMod'] > 1 && $log_time >= $today[$tzid] - 86400)
747
		{
748
			$prefix = $txt['yesterday'];
749
		}
750
	}
751
752
	// 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.
753
	$format = !is_bool($show_today) ? $show_today : $user_info['time_format'];
754
755
	$format = !empty($prefix) ? get_date_or_time_format('time', $format) : $format;
756
757
	// And now, the moment we've all be waiting for...
758
	return $prefix . smf_strftime($format, $log_time, $tzid);
759
}
760
761
/**
762
 * Gets a version of a strftime() format that only shows the date or time components
763
 *
764
 * @param string $type Either 'date' or 'time'.
765
 * @param string $format A strftime() format to process. Defaults to $user_info['time_format'].
766
 * @return string A strftime() format string
767
 */
768
function get_date_or_time_format($type = '', $format = '')
769
{
770
	global $user_info, $modSettings;
771
	static $formats;
772
773
	// If the format is invalid, fall back to defaults.
774
	if (strpos($format, '%') === false)
775
		$format = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %k:%M');
776
777
	$orig_format = $format;
778
779
	// Have we already done this?
780
	if (isset($formats[$orig_format][$type]))
781
		return $formats[$orig_format][$type];
782
783
	if ($type === 'date')
784
	{
785
		$specifications = array(
786
			// Day
787
			'%a' => '%a', '%A' => '%A', '%e' => '%e', '%d' => '%d', '%j' => '%j', '%u' => '%u', '%w' => '%w',
788
			// Week
789
			'%U' => '%U', '%V' => '%V', '%W' => '%W',
790
			// Month
791
			'%b' => '%b', '%B' => '%B', '%h' => '%h', '%m' => '%m',
792
			// Year
793
			'%C' => '%C', '%g' => '%g', '%G' => '%G', '%y' => '%y', '%Y' => '%Y',
794
			// Time
795
			'%H' => '', '%k' => '', '%I' => '', '%l' => '', '%M' => '', '%p' => '', '%P' => '',
796
			'%r' => '', '%R' => '', '%S' => '', '%T' => '', '%X' => '', '%z' => '', '%Z' => '',
797
			// Time and Date Stamps
798
			'%c' => '%x', '%D' => '%D', '%F' => '%F', '%s' => '%s', '%x' => '%x',
799
			// Miscellaneous
800
			'%n' => '', '%t' => '', '%%' => '%%',
801
		);
802
803
		$default_format = '%F';
804
	}
805
	elseif ($type === 'time')
806
	{
807
		$specifications = array(
808
			// Day
809
			'%a' => '', '%A' => '', '%e' => '', '%d' => '', '%j' => '', '%u' => '', '%w' => '',
810
			// Week
811
			'%U' => '', '%V' => '', '%W' => '',
812
			// Month
813
			'%b' => '', '%B' => '', '%h' => '', '%m' => '',
814
			// Year
815
			'%C' => '', '%g' => '', '%G' => '', '%y' => '', '%Y' => '',
816
			// Time
817
			'%H' => '%H', '%k' => '%k', '%I' => '%I', '%l' => '%l', '%M' => '%M', '%p' => '%p', '%P' => '%P',
818
			'%r' => '%r', '%R' => '%R', '%S' => '%S', '%T' => '%T', '%X' => '%X', '%z' => '%z', '%Z' => '%Z',
819
			// Time and Date Stamps
820
			'%c' => '%X', '%D' => '', '%F' => '', '%s' => '%s', '%x' => '',
821
			// Miscellaneous
822
			'%n' => '', '%t' => '', '%%' => '%%',
823
		);
824
825
		$default_format = '%k:%M';
826
	}
827
	// Invalid type requests just get the full format string.
828
	else
829
		return $format;
830
831
	// Separate the specifications we want from the ones we don't.
832
	$wanted = array_filter($specifications);
833
	$unwanted = array_diff(array_keys($specifications), $wanted);
834
835
	// First, make any necessary substitutions in the format.
836
	$format = strtr($format, $wanted);
837
838
	// Next, strip out any specifications and literal text that we don't want.
839
	$format_parts = preg_split('~%[' . (strtr(implode('', $unwanted), array('%' => ''))) . ']~u', $format);
840
841
	foreach ($format_parts as $p => $f)
842
	{
843
		if (strpos($f, '%') === false)
844
			unset($format_parts[$p]);
845
	}
846
847
	$format = implode('', $format_parts);
848
849
	// Finally, strip out any unwanted leftovers.
850
	// 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
851
	$format = preg_replace(
852
		array(
853
			// Anything that isn't a specification, punctuation mark, or whitespace.
854
			'~(?<!%)\p{L}|[^\p{L}\p{P}\s]~u',
855
			// Repeated punctuation marks (except %), possibly separated by whitespace.
856
			'~(?'.'>([^%\P{P}])\s*(?=\1))*~u',
857
			'~([^%\P{P}])(?'.'>\1(?!$))*~u',
858
			// Unwanted trailing punctuation and whitespace.
859
			'~(?'.'>([\p{Pd}\p{Ps}\p{Pi}\p{Pc}]|[^%\P{Po}])\s*)*$~u',
860
			// Unwanted opening punctuation and whitespace.
861
			'~^\s*(?'.'>([\p{Pd}\p{Pe}\p{Pf}\p{Pc}]|[^%\P{Po}])\s*)*~u',
862
			// Runs of horizontal whitespace.
863
			'~\s+~',
864
		),
865
		array(
866
			'',
867
			'$1',
868
			'$1$2',
869
			'',
870
			'',
871
			' ',
872
		),
873
		$format
874
	);
875
876
	// Gotta have something...
877
	if (empty($format))
878
		$format = $default_format;
879
880
	// Remember what we've done.
881
	$formats[$orig_format][$type] = trim($format);
882
883
	return $formats[$orig_format][$type];
884
}
885
886
/**
887
 * Replacement for strftime() that is compatible with PHP 8.1+.
888
 *
889
 * This does not use the system's strftime library or locale setting,
890
 * so results may vary in a few cases from the results of strftime():
891
 *
892
 *  - %a, %A, %b, %B, %p, %P: Output will use SMF's language strings
893
 *    to localize these values. If SMF's language strings have not
894
 *    been loaded, PHP's default English strings will be used.
895
 *
896
 *  - %c, %x, %X: Output will always use ISO format.
897
 *
898
 * @param string $format A strftime() format string.
899
 * @param int|null $timestamp A Unix timestamp.
900
 *     If null, defaults to the current time.
901
 * @param string|null $tzid Time zone identifier.
902
 *     If null, uses default time zone.
903
 * @return string The formatted datetime string.
904
 */
905
function smf_strftime(string $format, $timestamp = null, $tzid = null)
906
{
907
	global $txt, $smcFunc, $sourcedir;
908
909
	static $dates = array();
910
911
	// Set default values as necessary.
912
	if (!isset($timestamp))
913
		$timestamp = time();
914
915
	if (!isset($tzid))
916
		$tzid = date_default_timezone_get();
917
918
	$timestamp = min(max($timestamp, PHP_INT_MIN), PHP_INT_MAX);
919
920
	// A few substitutions to make life easier.
921
	$format = strtr($format, array(
922
		'%h' => '%b',
923
		'%r' => '%I:%M:%S %p',
924
		'%R' => '%H:%M',
925
		'%T' => '%H:%M:%S',
926
		'%X' => '%H:%M:%S',
927
		'%D' => '%m/%d/%y',
928
		'%F' => '%Y-%m-%d',
929
		'%x' => '%Y-%m-%d',
930
	));
931
932
	// Avoid unnecessary repetition.
933
	if (isset($dates[$tzid . '_' . $timestamp]['results'][$format]))
934
		return $dates[$tzid . '_' . $timestamp]['results'][$format];
935
936
	// Ensure the TZID is valid.
937
	if (($tz = @timezone_open($tzid)) === false)
938
	{
939
		$tzid = date_default_timezone_get();
940
941
		// Check again now that we have a valid TZID.
942
		if (isset($dates[$tzid . '_' . $timestamp]['results'][$format]))
943
			return $dates[$tzid . '_' . $timestamp]['results'][$format];
944
945
		$tz = timezone_open($tzid);
946
	}
947
948
	// Create the DateTime object and set its time zone.
949
	if (!isset($dates[$tzid . '_' . $timestamp]['object']))
950
	{
951
		$dates[$tzid . '_' . $timestamp]['object'] = date_create('@' . $timestamp);
952
		date_timezone_set($dates[$tzid . '_' . $timestamp]['object'], $tz);
953
	}
954
955
	// In case this function is called before reloadSettings().
956
	if (!isset($smcFunc['strtoupper']))
957
	{
958
		if (isset($sourcedir))
959
		{
960
			require_once($sourcedir . '/Subs-Charset.php');
961
			$smcFunc['strtoupper'] = 'utf8_strtoupper';
962
			$smcFunc['strtolower'] = 'utf8_strtolower';
963
		}
964
		elseif (function_exists('mb_strtoupper'))
965
		{
966
			$smcFunc['strtoupper'] = 'mb_strtoupper';
967
			$smcFunc['strtolower'] = 'mb_strtolower';
968
		}
969
		else
970
		{
971
			$smcFunc['strtoupper'] = 'strtoupper';
972
			$smcFunc['strtolower'] = 'strtolower';
973
		}
974
	}
975
976
	$format_equivalents = array(
977
		// Day
978
		'a' => 'D', // Complex: prefer $txt strings if available.
979
		'A' => 'l', // Complex: prefer $txt strings if available.
980
		'e' => 'j', // Complex: sprintf to prepend whitespace.
981
		'd' => 'd',
982
		'j' => 'z', // Complex: must add one and then sprintf to prepend zeros.
983
		'u' => 'N',
984
		'w' => 'w',
985
		// Week
986
		'U' => 'z_w_0', // Complex: calculated from these other values.
987
		'V' => 'W',
988
		'W' => 'z_w_1', // Complex: calculated from these other values.
989
		// Month
990
		'b' => 'M', // Complex: prefer $txt strings if available.
991
		'B' => 'F', // Complex: prefer $txt strings if available.
992
		'm' => 'm',
993
		// Year
994
		'C' => 'Y', // Complex: Get 'Y' then truncate to first two digits.
995
		'g' => 'o', // Complex: Get 'o' then truncate to last two digits.
996
		'G' => 'o', // Complex: Get 'o' then sprintf to ensure four digits.
997
		'y' => 'y',
998
		'Y' => 'Y',
999
		// Time
1000
		'H' => 'H',
1001
		'k' => 'G',
1002
		'I' => 'h',
1003
		'l' => 'g', // Complex: sprintf to prepend whitespace.
1004
		'M' => 'i',
1005
		'p' => 'A', // Complex: prefer $txt strings if available.
1006
		'P' => 'a', // Complex: prefer $txt strings if available.
1007
		'S' => 's',
1008
		'z' => 'O',
1009
		'Z' => 'T',
1010
		// Time and Date Stamps
1011
		'c' => 'c',
1012
		's' => 'U',
1013
		// Miscellaneous
1014
		'n' => "\n",
1015
		't' => "\t",
1016
		'%' => '%',
1017
	);
1018
1019
	// Translate from strftime format to DateTime format.
1020
	$parts = preg_split('/%(' . implode('|', array_keys($format_equivalents)) . ')/', $format, 0, PREG_SPLIT_DELIM_CAPTURE);
1021
1022
	$placeholders = array();
1023
	$complex = false;
1024
1025
	for ($i = 0; $i < count($parts); $i++)
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
1026
	{
1027
		// Parts that are not strftime formats.
1028
		if ($i % 2 === 0 || !isset($format_equivalents[$parts[$i]]))
1029
		{
1030
			if ($parts[$i] === '')
1031
				continue;
1032
1033
			$placeholder = "\xEE\x84\x80" . $i . "\xEE\x84\x81";
1034
1035
			$placeholders[$placeholder] = $parts[$i];
1036
			$parts[$i] = $placeholder;
1037
		}
1038
		// Parts that need localized strings.
1039
		elseif (in_array($parts[$i], array('a', 'A', 'b', 'B')))
1040
		{
1041
			switch ($parts[$i])
1042
			{
1043
				case 'a':
1044
					$min = 0;
1045
					$max = 6;
1046
					$key = 'days_short';
1047
					$f = 'w';
1048
					$placeholder_end = "\xEE\x84\x83";
1049
1050
					break;
1051
1052
				case 'A':
1053
					$min = 0;
1054
					$max = 6;
1055
					$key = 'days';
1056
					$f = 'w';
1057
					$placeholder_end = "\xEE\x84\x82";
1058
1059
					break;
1060
1061
				case 'b':
1062
					$min = 1;
1063
					$max = 12;
1064
					$key = 'months_short';
1065
					$f = 'n';
1066
					$placeholder_end = "\xEE\x84\x85";
1067
1068
					break;
1069
1070
				case 'B':
1071
					$min = 1;
1072
					$max = 12;
1073
					$key = 'months';
1074
					$f = 'n';
1075
					$placeholder_end = "\xEE\x84\x84";
1076
1077
					break;
1078
			}
1079
1080
			$placeholder = "\xEE\x84\x80" . $f . $placeholder_end;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $f does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $placeholder_end does not seem to be defined for all execution paths leading up to this point.
Loading history...
1081
1082
			// Check whether $txt contains all expected strings.
1083
			// If not, use English default.
1084
			$txt_strings_exist = true;
1085
			for ($num = $min; $num <= $max; $num++)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $min does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $max does not seem to be defined for all execution paths leading up to this point.
Loading history...
1086
			{
1087
				if (!isset($txt[$key][$num]))
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...
1088
				{
1089
					$txt_strings_exist = false;
1090
					break;
1091
				}
1092
				else
1093
					$placeholders[str_replace($f, $num, $placeholder)] = $txt[$key][$num];
1094
			}
1095
1096
			$parts[$i] = $txt_strings_exist ? $placeholder : $format_equivalents[$parts[$i]];
1097
		}
1098
		elseif (in_array($parts[$i], array('p', 'P')))
1099
		{
1100
			if (!isset($txt['time_am']) || !isset($txt['time_pm']))
1101
				continue;
1102
1103
			$placeholder = "\xEE\x84\x90" . $format_equivalents[$parts[$i]] . "\xEE\x84\x91";
1104
1105
			switch ($parts[$i])
1106
			{
1107
				// Lower case
1108
				case 'p':
1109
					$placeholders[str_replace($format_equivalents[$parts[$i]], 'AM', $placeholder)] = $smcFunc['strtoupper']($txt['time_am']);
1110
					$placeholders[str_replace($format_equivalents[$parts[$i]], 'PM', $placeholder)] = $smcFunc['strtoupper']($txt['time_pm']);
1111
					break;
1112
1113
				// Upper case
1114
				case 'P':
1115
					$placeholders[str_replace($format_equivalents[$parts[$i]], 'am', $placeholder)] = $smcFunc['strtolower']($txt['time_am']);
1116
					$placeholders[str_replace($format_equivalents[$parts[$i]], 'pm', $placeholder)] = $smcFunc['strtolower']($txt['time_pm']);
1117
					break;
1118
			}
1119
1120
			$parts[$i] = $placeholder;
1121
		}
1122
		// Parts that will need further processing.
1123
		elseif (in_array($parts[$i], array('j', 'C', 'U', 'W', 'G', 'g', 'e', 'l')))
1124
		{
1125
			$complex = true;
1126
1127
			switch ($parts[$i])
1128
			{
1129
				case 'j':
1130
					$placeholder_end = "\xEE\x84\xA1";
1131
					break;
1132
1133
				case 'C':
1134
					$placeholder_end = "\xEE\x84\xA2";
1135
					break;
1136
1137
				case 'U':
1138
				case 'W':
1139
					$placeholder_end = "\xEE\x84\xA3";
1140
					break;
1141
1142
				case 'G':
1143
					$placeholder_end = "\xEE\x84\xA4";
1144
					break;
1145
1146
				case 'g':
1147
					$placeholder_end = "\xEE\x84\xA5";
1148
					break;
1149
1150
				case 'e':
1151
				case 'l':
1152
					$placeholder_end = "\xEE\x84\xA6";
1153
			}
1154
1155
			$parts[$i] = "\xEE\x84\xA0" . $format_equivalents[$parts[$i]] . $placeholder_end;
1156
		}
1157
		// Parts with simple equivalents.
1158
		else
1159
			$parts[$i] = $format_equivalents[$parts[$i]];
1160
	}
1161
1162
	// The main event.
1163
	$dates[$tzid . '_' . $timestamp]['results'][$format] = strtr(date_format($dates[$tzid . '_' . $timestamp]['object'], implode('', $parts)), $placeholders);
1164
1165
	// Deal with the complicated ones.
1166
	if ($complex)
0 ignored issues
show
The condition $complex is always false.
Loading history...
1167
	{
1168
		$dates[$tzid . '_' . $timestamp]['results'][$format] = preg_replace_callback(
1169
			'/\xEE\x84\xA0([\d_]+)(\xEE\x84(?:[\xA1-\xAF]))/',
1170
			function ($matches)
1171
			{
1172
				switch ($matches[2])
1173
				{
1174
					// %j
1175
					case "\xEE\x84\xA1":
1176
						$replacement = sprintf('%03d', (int) $matches[1] + 1);
1177
						break;
1178
1179
					// %C
1180
					case "\xEE\x84\xA2":
1181
						$replacement = substr(sprintf('%04d', $matches[1]), 0, 2);
1182
						break;
1183
1184
					// %U and %W
1185
					case "\xEE\x84\xA3":
1186
						list($day_of_year, $day_of_week, $first_day) = explode('_', $matches[1]);
1187
						$replacement = sprintf('%02d', floor(((int) $day_of_year - (int) $day_of_week + (int) $first_day) / 7) + 1);
1188
						break;
1189
1190
					// %G
1191
					case "\xEE\x84\xA4":
1192
						$replacement = sprintf('%04d', $matches[1]);
1193
						break;
1194
1195
					// %g
1196
					case "\xEE\x84\xA5":
1197
						$replacement = substr(sprintf('%04d', $matches[1]), -2);
1198
						break;
1199
1200
					// %e and %l
1201
					case "\xEE\x84\xA6":
1202
						$replacement = sprintf('%2d', $matches[1]);
1203
						break;
1204
1205
					// Shouldn't happen, but just in case...
1206
					default:
1207
						$replacement = $matches[1];
1208
						break;
1209
				}
1210
1211
				return $replacement;
1212
			},
1213
			$dates[$tzid . '_' . $timestamp]['results'][$format]
1214
		);
1215
	}
1216
1217
	return $dates[$tzid . '_' . $timestamp]['results'][$format];
1218
}
1219
1220
/**
1221
 * Replacement for gmstrftime() that is compatible with PHP 8.1+.
1222
 *
1223
 * Calls smf_strftime() with the $tzid parameter set to 'UTC'.
1224
 *
1225
 * @param string $format A strftime() format string.
1226
 * @param int|null $timestamp A Unix timestamp.
1227
 *     If null, defaults to the current time.
1228
 * @return string The formatted datetime string.
1229
 */
1230
function smf_gmstrftime(string $format, $timestamp = null)
1231
{
1232
	return smf_strftime($format, $timestamp, 'UTC');
1233
}
1234
1235
/**
1236
 * Replaces special entities in strings with the real characters.
1237
 *
1238
 * Functionally equivalent to htmlspecialchars_decode(), except that this also
1239
 * replaces '&nbsp;' with a simple space character.
1240
 *
1241
 * @param string $string A string
1242
 * @return string The string without entities
1243
 */
1244
function un_htmlspecialchars($string)
1245
{
1246
	global $context;
1247
	static $translation = array();
1248
1249
	// Determine the character set... Default to UTF-8
1250
	if (empty($context['character_set']))
1251
		$charset = 'UTF-8';
1252
	// Use ISO-8859-1 in place of non-supported ISO-8859 charsets...
1253
	elseif (strpos($context['character_set'], 'ISO-8859-') !== false && !in_array($context['character_set'], array('ISO-8859-5', 'ISO-8859-15')))
1254
		$charset = 'ISO-8859-1';
1255
	else
1256
		$charset = $context['character_set'];
1257
1258
	if (empty($translation))
1259
		$translation = array_flip(get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES, $charset)) + array('&#039;' => '\'', '&#39;' => '\'', '&nbsp;' => ' ');
1260
1261
	return strtr($string, $translation);
1262
}
1263
1264
/**
1265
 * Replaces invalid characters with a substitute.
1266
 *
1267
 * !!! Warning !!! Setting $substitute to '' in order to delete invalid
1268
 * characters from the string can create unexpected security problems. See
1269
 * https://www.unicode.org/reports/tr36/#Deletion_of_Noncharacters for an
1270
 * explanation.
1271
 *
1272
 * @param string $string The string to sanitize.
1273
 * @param int $level Controls filtering of invisible formatting characters.
1274
 *      0: Allow valid formatting characters. Use for sanitizing text in posts.
1275
 *      1: Allow necessary formatting characters. Use for sanitizing usernames.
1276
 *      2: Disallow all formatting characters. Use for internal comparisions
1277
 *         only, such as in the word censor, search contexts, etc.
1278
 *      Default: 0.
1279
 * @param string|null $substitute Replacement string for the invalid characters.
1280
 *      If not set, the Unicode replacement character (U+FFFD) will be used
1281
 *      (or a fallback like "?" if necessary).
1282
 * @return string The sanitized string.
1283
 */
1284
function sanitize_chars($string, $level = 0, $substitute = null)
1285
{
1286
	global $context, $sourcedir;
1287
1288
	$string = (string) $string;
1289
	$level = min(max((int) $level, 0), 2);
1290
1291
	// What substitute character should we use?
1292
	if (isset($substitute))
1293
	{
1294
		$substitute = strval($substitute);
1295
	}
1296
	elseif (!empty($context['utf8']))
1297
	{
1298
		// Raw UTF-8 bytes for U+FFFD.
1299
		$substitute = "\xEF\xBF\xBD";
1300
	}
1301
	elseif (!empty($context['character_set']) && is_callable('mb_decode_numericentity'))
1302
	{
1303
		// Get whatever the default replacement character is for this encoding.
1304
		$substitute = mb_decode_numericentity('&#xFFFD;', array(0xFFFD,0xFFFD,0,0xFFFF), $context['character_set']);
1305
	}
1306
	else
1307
		$substitute = '?';
1308
1309
	// Fix any invalid byte sequences.
1310
	if (!empty($context['character_set']))
1311
	{
1312
		// For UTF-8, this preg_match test is much faster than mb_check_encoding.
1313
		$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']));
1314
1315
		if ($malformed)
1316
		{
1317
			// mb_convert_encoding will replace invalid byte sequences with our substitute.
1318
			if (is_callable('mb_convert_encoding'))
1319
			{
1320
				if (!is_callable('mb_ord'))
1321
					require_once($sourcedir . '/Subs-Compat.php');
1322
1323
				$substitute_ord = $substitute === '' ? 'none' : mb_ord($substitute, $context['character_set']);
0 ignored issues
show
It seems like $substitute can also be of type null; however, parameter $string of mb_ord() 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

1323
				$substitute_ord = $substitute === '' ? 'none' : mb_ord(/** @scrutinizer ignore-type */ $substitute, $context['character_set']);
Loading history...
1324
1325
				$mb_substitute_character = mb_substitute_character();
1326
				mb_substitute_character($substitute_ord);
1327
1328
				$string = mb_convert_encoding($string, $context['character_set'], $context['character_set']);
1329
1330
				mb_substitute_character($mb_substitute_character);
0 ignored issues
show
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

1330
				mb_substitute_character(/** @scrutinizer ignore-type */ $mb_substitute_character);
Loading history...
1331
			}
1332
			else
1333
				return false;
1334
		}
1335
	}
1336
1337
	// Fix any weird vertical space characters.
1338
	$string = normalize_spaces($string, true);
0 ignored issues
show
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

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

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

2661
		$alltags_regex = '(?' . '>\b' . build_regex(array_unique($alltags)) . '\b|' . /** @scrutinizer ignore-type */ build_regex(array_keys($itemcodes)) . ')';
Loading history...
2662
	}
2663
2664
	$pos = -1;
2665
	while ($pos !== false)
2666
	{
2667
		$last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos;
2668
		preg_match('~\[/?(?=' . $alltags_regex . ')~i', $message, $matches, PREG_OFFSET_CAPTURE, $pos + 1);
2669
		$pos = isset($matches[0][1]) ? $matches[0][1] : false;
2670
2671
		// Failsafe.
2672
		if ($pos === false || $last_pos > $pos)
2673
			$pos = strlen($message) + 1;
2674
2675
		// Can't have a one letter smiley, URL, or email! (Sorry.)
2676
		if ($last_pos < $pos - 1)
2677
		{
2678
			// Make sure the $last_pos is not negative.
2679
			$last_pos = max($last_pos, 0);
2680
2681
			// Pick a block of data to do some raw fixing on.
2682
			$data = substr($message, $last_pos, $pos - $last_pos);
2683
2684
			$placeholders = array();
2685
			$placeholders_counter = 0;
2686
			// Wrap in "private use" Unicode characters to ensure there will be no conflicts.
2687
			$placeholder_template = html_entity_decode('&#xE03C;') . '%1$s' . html_entity_decode('&#xE03E;');
2688
2689
			// Take care of some HTML!
2690
			if (!empty($modSettings['enablePostHTML']) && strpos($data, '&lt;') !== false)
2691
			{
2692
				$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);
2693
2694
				// <br> should be empty.
2695
				$empty_tags = array('br', 'hr');
2696
				foreach ($empty_tags as $tag)
2697
					$data = str_replace(array('&lt;' . $tag . '&gt;', '&lt;' . $tag . '/&gt;', '&lt;' . $tag . ' /&gt;'), '<' . $tag . '>', $data);
2698
2699
				// b, u, i, s, pre... basic tags.
2700
				$closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote', 'strong');
2701
				foreach ($closable_tags as $tag)
2702
				{
2703
					$diff = substr_count($data, '&lt;' . $tag . '&gt;') - substr_count($data, '&lt;/' . $tag . '&gt;');
2704
					$data = strtr($data, array('&lt;' . $tag . '&gt;' => '<' . $tag . '>', '&lt;/' . $tag . '&gt;' => '</' . $tag . '>'));
2705
2706
					if ($diff > 0)
2707
						$data = substr($data, 0, -1) . str_repeat('</' . $tag . '>', $diff) . substr($data, -1);
2708
				}
2709
2710
				// Do <img ...> - with security... action= -> action-.
2711
				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);
2712
				if (!empty($matches[0]))
2713
				{
2714
					$replaces = array();
2715
					foreach ($matches[2] as $match => $imgtag)
2716
					{
2717
						$alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^&quot;|&quot;$~', '', $matches[3][$match]);
2718
2719
						// Remove action= from the URL - no funny business, now.
2720
						// @todo Testing this preg_match seems pointless
2721
						if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) != 0)
2722
							$imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag);
2723
2724
						$placeholder = sprintf($placeholder_template, ++$placeholders_counter);
2725
						$placeholders[$placeholder] = '[img' . $alt . ']' . $imgtag . '[/img]';
2726
2727
						$replaces[$matches[0][$match]] = $placeholder;
2728
					}
2729
2730
					$data = strtr($data, $replaces);
2731
				}
2732
			}
2733
2734
			if (!empty($modSettings['autoLinkUrls']))
2735
			{
2736
				// Are we inside tags that should be auto linked?
2737
				$no_autolink_area = false;
2738
				if (!empty($open_tags))
2739
				{
2740
					foreach ($open_tags as $open_tag)
2741
						if (in_array($open_tag['tag'], $no_autolink_tags))
2742
							$no_autolink_area = true;
2743
				}
2744
2745
				// Don't go backwards.
2746
				// @todo Don't think is the real solution....
2747
				$lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0;
2748
				if ($pos < $lastAutoPos)
2749
					$no_autolink_area = true;
2750
				$lastAutoPos = $pos;
2751
2752
				if (!$no_autolink_area)
2753
				{
2754
					// An &nbsp; right after a URL can break the autolinker
2755
					if (strpos($data, '&nbsp;') !== false)
2756
					{
2757
						$placeholders[html_entity_decode('&nbsp;', 0, $context['character_set'])] = '&nbsp;';
2758
						$data = strtr($data, array('&nbsp;' => html_entity_decode('&nbsp;', 0, $context['character_set'])));
2759
					}
2760
2761
					// Some reusable character classes
2762
					$excluded_trailing_chars = '!;:.,?';
2763
					$domain_label_chars = '0-9A-Za-z\-' . ($context['utf8'] ? implode('', array(
2764
						'\x{A0}-\x{D7FF}', '\x{F900}-\x{FDCF}', '\x{FDF0}-\x{FFEF}',
2765
						'\x{10000}-\x{1FFFD}', '\x{20000}-\x{2FFFD}', '\x{30000}-\x{3FFFD}',
2766
						'\x{40000}-\x{4FFFD}', '\x{50000}-\x{5FFFD}', '\x{60000}-\x{6FFFD}',
2767
						'\x{70000}-\x{7FFFD}', '\x{80000}-\x{8FFFD}', '\x{90000}-\x{9FFFD}',
2768
						'\x{A0000}-\x{AFFFD}', '\x{B0000}-\x{BFFFD}', '\x{C0000}-\x{CFFFD}',
2769
						'\x{D0000}-\x{DFFFD}', '\x{E1000}-\x{EFFFD}',
2770
					)) : '');
2771
2772
					// Parse any URLs
2773
					if (!isset($disabled['url']) && strpos($data, '[url') === false)
2774
					{
2775
						// URI schemes that require some sort of special handling.
2776
						$schemes = array(
2777
							// Schemes whose URI definitions require a domain name in the
2778
							// authority (or whatever the next part of the URI is).
2779
							'need_domain' => array(
2780
								'aaa', 'aaas', 'acap', 'acct', 'afp', 'cap', 'cid', 'coap',
2781
								'coap+tcp', 'coap+ws', 'coaps', 'coaps+tcp', 'coaps+ws', 'crid',
2782
								'cvs', 'dict', 'dns', 'feed', 'fish', 'ftp', 'git', 'go',
2783
								'gopher', 'h323', 'http', 'https', 'iax', 'icap', 'im', 'imap',
2784
								'ipp', 'ipps', 'irc', 'irc6', 'ircs', 'ldap', 'ldaps', 'mailto',
2785
								'mid', 'mupdate', 'nfs', 'nntp', 'pop', 'pres', 'reload',
2786
								'rsync', 'rtsp', 'sftp', 'sieve', 'sip', 'sips', 'smb', 'snmp',
2787
								'soap.beep', 'soap.beeps', 'ssh', 'svn', 'stun', 'stuns',
2788
								'telnet', 'tftp', 'tip', 'tn3270', 'turn', 'turns', 'tv', 'udp',
2789
								'vemmi', 'vnc', 'webcal', 'ws', 'wss', 'xmlrpc.beep',
2790
								'xmlrpc.beeps', 'xmpp', 'z39.50', 'z39.50r', 'z39.50s',
2791
							),
2792
							// Schemes that allow an empty authority ("://" followed by "/")
2793
							'empty_authority' => array(
2794
								'file', 'ni', 'nih',
2795
							),
2796
							// Schemes that do not use an authority but still have a reasonable
2797
							// chance of working as clickable links.
2798
							'no_authority' => array(
2799
								'about', 'callto', 'geo', 'gg', 'leaptofrogans', 'magnet',
2800
								'mailto', 'maps', 'news', 'ni', 'nih', 'service', 'skype',
2801
								'sms', 'tel', 'tv',
2802
							),
2803
							// Schemes that we should never link.
2804
							'forbidden' => array(
2805
								'javascript', 'data',
2806
							),
2807
						);
2808
2809
						// In case a mod wants to control behaviour for a special URI scheme.
2810
						call_integration_hook('integrate_autolinker_schemes', array(&$schemes));
2811
2812
						// Don't repeat this unnecessarily.
2813
						if (empty($url_regex))
2814
						{
2815
							// PCRE subroutines for efficiency.
2816
							$pcre_subroutines = array(
2817
								'tlds' => $modSettings['tld_regex'],
2818
								'pct' => '%[0-9A-Fa-f]{2}',
2819
								'domain_label_char' => '[' . $domain_label_chars . ']',
2820
								'not_domain_label_char' => '[^' . $domain_label_chars . ']',
2821
								'domain' => '(?:(?P>domain_label_char)+\.)+(?P>tlds)(?!\.(?P>domain_label_char))',
2822
								'no_domain' => '(?:(?P>domain_label_char)|[._\~!$&\'()*+,;=:@]|(?P>pct))+',
2823
								'scheme_need_domain' => build_regex($schemes['need_domain'], '~'),
2824
								'scheme_empty_authority' => build_regex($schemes['empty_authority'], '~'),
2825
								'scheme_no_authority' => build_regex($schemes['no_authority'], '~'),
2826
								'scheme_any' => '[A-Za-z][0-9A-Za-z+\-.]*',
2827
								'user_info' => '(?:(?P>domain_label_char)|[._\~!$&\'()*+,;=:]|(?P>pct))+',
2828
								'dec_octet' => '(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)',
2829
								'h16' => '[0-9A-Fa-f]{1,4}',
2830
								'ipv4' => '(?:\b(?:(?P>dec_octet)\.){3}(?P>dec_octet)\b)',
2831
								'ipv6' => '\[(?:' . implode('|', array(
2832
									'(?:(?P>h16):){7}(?P>h16)',
2833
									'(?:(?P>h16):){1,7}:',
2834
									'(?:(?P>h16):){1,6}(?::(?P>h16))',
2835
									'(?:(?P>h16):){1,5}(?::(?P>h16)){1,2}',
2836
									'(?:(?P>h16):){1,4}(?::(?P>h16)){1,3}',
2837
									'(?:(?P>h16):){1,3}(?::(?P>h16)){1,4}',
2838
									'(?:(?P>h16):){1,2}(?::(?P>h16)){1,5}',
2839
									'(?P>h16):(?::(?P>h16)){1,6}',
2840
									':(?:(?::(?P>h16)){1,7}|:)',
2841
									'fe80:(?::(?P>h16)){0,4}%[0-9A-Za-z]+',
2842
									'::(ffff(:0{1,4})?:)?(?P>ipv4)',
2843
									'(?:(?P>h16):){1,4}:(?P>ipv4)',
2844
								)) . ')\]',
2845
								'host' => '(?:' . implode('|', array(
2846
									'localhost',
2847
									'(?P>domain)',
2848
									'(?P>ipv4)',
2849
									'(?P>ipv6)',
2850
								)) . ')',
2851
								'authority' => '(?:(?P>user_info)@)?(?P>host)(?::\d+)?',
2852
							);
2853
2854
							// Brackets and quotation marks are problematic at the end of an IRI.
2855
							// E.g.: `http://foo.com/baz(qux)` vs. `(http://foo.com/baz_qux)`
2856
							// In the first case, the user probably intended the `)` as part of the
2857
							// IRI, but not in the second case. To account for this, we test for
2858
							// balanced pairs within the IRI.
2859
							$balanced_pairs = array(
2860
								// Brackets and parentheses
2861
								'(' => ')', '[' => ']', '{' => '}',
2862
								// Double quotation marks
2863
								'"' => '"',
2864
								html_entity_decode('&#x201C;', 0, $context['character_set']) => html_entity_decode('&#x201D;', 0, $context['character_set']),
2865
								html_entity_decode('&#x201E;', 0, $context['character_set']) => html_entity_decode('&#x201D;', 0, $context['character_set']),
2866
								html_entity_decode('&#x201F;', 0, $context['character_set']) => html_entity_decode('&#x201D;', 0, $context['character_set']),
2867
								html_entity_decode('&#x00AB;', 0, $context['character_set']) => html_entity_decode('&#x00BB;', 0, $context['character_set']),
2868
								// Single quotation marks
2869
								'\'' => '\'',
2870
								html_entity_decode('&#x2018;', 0, $context['character_set']) => html_entity_decode('&#x2019;', 0, $context['character_set']),
2871
								html_entity_decode('&#x201A;', 0, $context['character_set']) => html_entity_decode('&#x2019;', 0, $context['character_set']),
2872
								html_entity_decode('&#x201B;', 0, $context['character_set']) => html_entity_decode('&#x2019;', 0, $context['character_set']),
2873
								html_entity_decode('&#x2039;', 0, $context['character_set']) => html_entity_decode('&#x203A;', 0, $context['character_set']),
2874
							);
2875
							foreach ($balanced_pairs as $pair_opener => $pair_closer)
2876
								$balanced_pairs[$smcFunc['htmlspecialchars']($pair_opener)] = $smcFunc['htmlspecialchars']($pair_closer);
2877
2878
							$bracket_quote_chars = '';
2879
							$bracket_quote_entities = array();
2880
							foreach ($balanced_pairs as $pair_opener => $pair_closer)
2881
							{
2882
								if ($pair_opener == $pair_closer)
2883
									$pair_closer = '';
2884
2885
								foreach (array($pair_opener, $pair_closer) as $bracket_quote)
2886
								{
2887
									if (strpos($bracket_quote, '&') === false)
2888
										$bracket_quote_chars .= $bracket_quote;
2889
									else
2890
										$bracket_quote_entities[] = substr($bracket_quote, 1);
2891
								}
2892
							}
2893
							$bracket_quote_chars = str_replace(array('[', ']'), array('\[', '\]'), $bracket_quote_chars);
2894
2895
							$pcre_subroutines['bracket_quote'] = '[' . $bracket_quote_chars . ']|&' . build_regex($bracket_quote_entities, '~');
0 ignored issues
show
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

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

2896
							$pcre_subroutines['allowed_entities'] = '&(?!' . /** @scrutinizer ignore-type */ build_regex(array_merge($bracket_quote_entities, array('lt;', 'gt;')), '~') . ')';
Loading history...
2897
							$pcre_subroutines['excluded_lookahead'] = '(?![' . $excluded_trailing_chars . ']*(?' . '>[\h\v]|<br>|$))';
2898
2899
							foreach (array('path', 'query', 'fragment') as $part)
2900
							{
2901
								switch ($part) {
2902
									case 'path':
2903
										$part_disallowed_chars = '\h\v<>' . $bracket_quote_chars . $excluded_trailing_chars . '/#&';
2904
										$part_excluded_trailing_chars = str_replace('?', '', $excluded_trailing_chars);
2905
										break;
2906
2907
									case 'query':
2908
										$part_disallowed_chars = '\h\v<>' . $bracket_quote_chars . $excluded_trailing_chars . '#&';
2909
										$part_excluded_trailing_chars = $excluded_trailing_chars;
2910
										break;
2911
2912
									default:
2913
										$part_disallowed_chars = '\h\v<>' . $bracket_quote_chars . $excluded_trailing_chars . '&';
2914
										$part_excluded_trailing_chars = $excluded_trailing_chars;
2915
										break;
2916
								}
2917
								$pcre_subroutines[$part . '_allowed'] = '[^' . $part_disallowed_chars . ']|(?P>allowed_entities)|[' . $part_excluded_trailing_chars . '](?P>excluded_lookahead)';
2918
2919
								$balanced_construct_regex = array();
2920
2921
								foreach ($balanced_pairs as $pair_opener => $pair_closer)
2922
									$balanced_construct_regex[] = preg_quote($pair_opener) . '(?P>' . $part . '_recursive)*+' . preg_quote($pair_closer);
2923
2924
								$pcre_subroutines[$part . '_balanced'] = '(?:' . implode('|', $balanced_construct_regex) . ')(?P>' . $part . '_allowed)*+';
2925
								$pcre_subroutines[$part . '_recursive'] = '(?' . '>(?P>' . $part . '_allowed)|(?P>' . $part . '_balanced))';
2926
2927
								$pcre_subroutines[$part . '_segment'] =
2928
									// Allowed characters besides brackets and quotation marks
2929
									'(?P>' . $part . '_allowed)*+' .
2930
									// Brackets and quotation marks that are either...
2931
									'(?:' .
2932
										// part of a balanced construct
2933
										'(?P>' . $part . '_balanced)' .
2934
										// or
2935
										'|' .
2936
										// unpaired but not at the end
2937
										'(?P>bracket_quote)(?=(?P>' . $part . '_allowed))' .
2938
									')*+';
2939
							}
2940
2941
							// Time to build this monster!
2942
							$url_regex =
2943
							// 1. IRI scheme and domain components
2944
							'(?:' .
2945
								// 1a. IRIs with a scheme, or at least an opening "//"
2946
								'(?:' .
2947
2948
									// URI scheme (or lack thereof for schemeless URLs)
2949
									'(?' . '>' .
2950
										// URI scheme and colon
2951
										'\b' .
2952
										'(?:' .
2953
											// Either a scheme that need a domain in the authority
2954
											// (Remember for later that we need a domain)
2955
											'(?P<need_domain>(?P>scheme_need_domain)):' .
2956
											// or
2957
											'|' .
2958
											// a scheme that allows an empty authority
2959
											// (Remember for later that the authority can be empty)
2960
											'(?P<empty_authority>(?P>scheme_empty_authority)):' .
2961
											// or
2962
											'|' .
2963
											// a scheme that uses no authority
2964
											'(?P>scheme_no_authority):(?!//)' .
2965
											// or
2966
											'|' .
2967
											// another scheme, but only if it is followed by "://"
2968
											'(?P>scheme_any):(?=//)' .
2969
										')' .
2970
2971
										// or
2972
										'|' .
2973
2974
										// An empty string followed by "//" for schemeless URLs
2975
										'(?P<schemeless>(?=//))' .
2976
									')' .
2977
2978
									// IRI authority chunk (maybe)
2979
									'(?:' .
2980
										// (Keep track of whether we find a valid authority or not)
2981
										'(?P<has_authority>' .
2982
											// 2 slashes before the authority itself
2983
											'//' .
2984
											'(?:' .
2985
												// If there was no scheme...
2986
												'(?(<schemeless>)' .
2987
													// require an authority that contains a domain.
2988
													'(?P>authority)' .
2989
2990
													// Else if a domain is needed...
2991
													'|(?(<need_domain>)' .
2992
														// require an authority with a domain.
2993
														'(?P>authority)' .
2994
2995
														// Else if an empty authority is allowed...
2996
														'|(?(<empty_authority>)' .
2997
															// then require either
2998
															'(?:' .
2999
																// empty string, followed by a "/"
3000
																'(?=/)' .
3001
																// or
3002
																'|' .
3003
																// an authority with a domain.
3004
																'(?P>authority)' .
3005
															')' .
3006
3007
															// Else just a run of IRI characters.
3008
															'|(?P>no_domain)' .
3009
														')' .
3010
													')' .
3011
												')' .
3012
											')' .
3013
											// Followed by a non-domain character or end of line
3014
											'(?=(?P>not_domain_label_char)|$)' .
3015
										')' .
3016
3017
										// or, if there is a scheme but no authority
3018
										// (e.g. "mailto:" URLs)...
3019
										'|' .
3020
3021
										// A run of IRI characters
3022
										'(?P>no_domain)' .
3023
										// If scheme needs a domain, require a dot and a TLD
3024
										'(?(<need_domain>)\.(?P>tlds))' .
3025
										// Followed by a non-domain character or end of line
3026
										'(?=(?P>not_domain_label_char)|$)' .
3027
									')' .
3028
								')' .
3029
3030
								// Or, if there is neither a scheme nor an authority...
3031
								'|' .
3032
3033
								// 1b. Naked domains
3034
								// (e.g. "example.com" in "Go to example.com for an example.")
3035
								'(?P<naked_domain>' .
3036
									// Preceded by start of line or a space
3037
									'(?<=^|<br>|[\h\v])' .
3038
									// A domain name
3039
									'(?P>domain)' .
3040
									// Followed by a non-domain character or end of line
3041
									'(?=(?P>not_domain_label_char)|$)' .
3042
								')' .
3043
							')' .
3044
3045
							// 2. IRI path, query, and fragment components (if present)
3046
							'(?:' .
3047
								// If the IRI has an authority or is a naked domain and any of these
3048
								// components exist, the path must start with a single "/".
3049
								// Note: technically, it is valid to append a query or fragment
3050
								// directly to the authority chunk without a "/", but supporting
3051
								// that in the autolinker would produce a lot of false positives,
3052
								// so we don't.
3053
								'(?=' .
3054
									// If we found an authority above...
3055
									'(?(<has_authority>)' .
3056
										// require a "/"
3057
										'/' .
3058
										// Else if we found a naked domain above...
3059
										'|(?(<naked_domain>)' .
3060
											// require a "/"
3061
											'/' .
3062
										')' .
3063
									')' .
3064
								')' .
3065
3066
								// 2.a. Path component, if any.
3067
								'(?:' .
3068
									// Can have one or more segments
3069
									'(?:' .
3070
										// Not preceded by a "/", except in the special case of an
3071
										// empty authority immediately before the path.
3072
										'(?(<empty_authority>)' .
3073
											'(?:(?<=://)|(?<!/))' .
3074
											'|' .
3075
											'(?<!/)' .
3076
										')' .
3077
										// Initial "/"
3078
										'/' .
3079
										// Then a run of allowed path segement characters
3080
										'(?P>path_segment)*+' .
3081
									')*+' .
3082
								')' .
3083
3084
								// 2.b. Query component, if any.
3085
								'(?:' .
3086
									// Initial "?" that is not last character.
3087
									'\?' . '(?=(?P>bracket_quote)*(?P>query_allowed))' .
3088
									// Then a run of allowed query characters
3089
									'(?P>query_segment)*+' .
3090
								')?' .
3091
3092
								// 2.c. Fragment component, if any.
3093
								'(?:' .
3094
									// Initial "#" that is not last character.
3095
									'#' . '(?=(?P>bracket_quote)*(?P>fragment_allowed))' .
3096
									// Then a run of allowed fragment characters
3097
									'(?P>fragment_segment)*+' .
3098
								')?' .
3099
							')?+';
3100
3101
							// Finally, define the PCRE subroutines in the regex.
3102
							$url_regex .= '(?(DEFINE)';
3103
3104
							foreach ($pcre_subroutines as $name => $subroutine)
3105
								$url_regex .= '(?<' . $name . '>' . $subroutine . ')';
0 ignored issues
show
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

3105
								$url_regex .= '(?<' . $name . '>' . /** @scrutinizer ignore-type */ $subroutine . ')';
Loading history...
3106
3107
							$url_regex .= ')';
3108
						}
3109
3110
						$tmp_data = preg_replace_callback(
3111
							'~' . $url_regex . '~i' . ($context['utf8'] ? 'u' : ''),
3112
							function($matches) use ($schemes)
3113
							{
3114
								$url = array_shift($matches);
3115
3116
								// If this isn't a clean URL, bail out
3117
								if ($url != sanitize_iri($url))
3118
									return $url;
3119
3120
								// Ensure the host name is in its canonical form.
3121
								$url = normalize_iri($url);
3122
3123
								$parsedurl = parse_iri($url);
3124
3125
								if (!isset($parsedurl['scheme']))
3126
									$parsedurl['scheme'] = '';
3127
3128
								if ($parsedurl['scheme'] == 'mailto')
3129
								{
3130
									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...
3131
										return $url;
3132
3133
									// Is this version of PHP capable of validating this email address?
3134
									$can_validate = defined('FILTER_FLAG_EMAIL_UNICODE') || strlen($parsedurl['path']) == strspn(strtolower($parsedurl['path']), 'abcdefghijklmnopqrstuvwxyz0123456789!#$%&\'*+-/=?^_`{|}~.@');
3135
3136
									$flags = defined('FILTER_FLAG_EMAIL_UNICODE') ? FILTER_FLAG_EMAIL_UNICODE : null;
3137
3138
									if (!$can_validate || filter_var($parsedurl['path'], FILTER_VALIDATE_EMAIL, $flags) !== false)
0 ignored issues
show
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

3138
									if (!$can_validate || filter_var($parsedurl['path'], FILTER_VALIDATE_EMAIL, /** @scrutinizer ignore-type */ $flags) !== false)
Loading history...
3139
										return '[email=' . str_replace('mailto:', '', $url) . ']' . $url . '[/email]';
3140
									else
3141
										return $url;
3142
								}
3143
3144
								// Are we linking a schemeless URL or naked domain name (e.g. "example.com")?
3145
								if (empty($parsedurl['scheme']))
3146
									$fullUrl = '//' . ltrim($url, ':/');
3147
								else
3148
									$fullUrl = $url;
3149
3150
								// Make sure that $fullUrl really is valid
3151
								if (in_array($parsedurl['scheme'], $schemes['forbidden']) || (!in_array($parsedurl['scheme'], $schemes['no_authority']) && validate_iri((strpos($fullUrl, '//') === 0 ? 'http:' : '') . $fullUrl) === false))
3152
									return $url;
3153
3154
								return '[url=&quot;' . str_replace(array('[', ']'), array('&#91;', '&#93;'), iri_to_url($fullUrl)) . '&quot;]' . $url . '[/url]';
3155
							},
3156
							$data
3157
						);
3158
3159
						if (!is_null($tmp_data))
3160
							$data = $tmp_data;
3161
					}
3162
3163
					// Next, emails...  Must be careful not to step on enablePostHTML logic above...
3164
					if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false && stripos($data, 'mailto:') === false)
3165
					{
3166
						// Preceded by a space or start of line
3167
						$email_regex = '(?<=^|<br>|[\h\v])' .
3168
3169
						// An email address
3170
						'[' . $domain_label_chars . '_.]{1,80}' .
3171
						'@' .
3172
						'[' . $domain_label_chars . '.]+' .
3173
						'\.' . $modSettings['tld_regex'] .
3174
3175
						// Followed by a non-domain character or end of line
3176
						'(?=[^' . $domain_label_chars . ']|$)';
3177
3178
						$tmp_data = preg_replace('~' . $email_regex . '~i' . ($context['utf8'] ? 'u' : ''), '[email]$0[/email]', $data);
3179
3180
						if (!is_null($tmp_data))
3181
							$data = $tmp_data;
3182
					}
3183
3184
					// Save a little memory.
3185
					unset($tmp_data);
3186
				}
3187
			}
3188
3189
			// Restore any placeholders
3190
			$data = strtr($data, $placeholders);
3191
3192
			$data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));
3193
3194
			// If it wasn't changed, no copying or other boring stuff has to happen!
3195
			if ($data != substr($message, $last_pos, $pos - $last_pos))
3196
			{
3197
				$message = substr($message, 0, $last_pos) . $data . substr($message, $pos);
3198
3199
				// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
3200
				$old_pos = strlen($data) + $last_pos;
3201
				$pos = strpos($message, '[', $last_pos);
3202
				$pos = $pos === false ? $old_pos : min($pos, $old_pos);
3203
			}
3204
		}
3205
3206
		// Are we there yet?  Are we there yet?
3207
		if ($pos >= strlen($message) - 1)
3208
			break;
3209
3210
		$tag_character = strtolower($message[$pos + 1]);
3211
3212
		if ($tag_character == '/' && !empty($open_tags))
3213
		{
3214
			$pos2 = strpos($message, ']', $pos + 1);
3215
			if ($pos2 == $pos + 2)
3216
				continue;
3217
3218
			$look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));
3219
3220
			// A closing tag that doesn't match any open tags? Skip it.
3221
			if (!in_array($look_for, array_map(function($code) { return $code['tag']; }, $open_tags)))
3222
				continue;
3223
3224
			$to_close = array();
3225
			$block_level = null;
3226
3227
			do
3228
			{
3229
				$tag = array_pop($open_tags);
3230
				if (!$tag)
3231
					break;
3232
3233
				if (!empty($tag['block_level']))
3234
				{
3235
					// Only find out if we need to.
3236
					if ($block_level === false)
3237
					{
3238
						array_push($open_tags, $tag);
3239
						break;
3240
					}
3241
3242
					// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
3243
					if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]]))
3244
					{
3245
						foreach ($bbc_codes[$look_for[0]] as $temp)
3246
							if ($temp['tag'] == $look_for)
3247
							{
3248
								$block_level = !empty($temp['block_level']);
3249
								break;
3250
							}
3251
					}
3252
3253
					if ($block_level !== true)
3254
					{
3255
						$block_level = false;
3256
						array_push($open_tags, $tag);
3257
						break;
3258
					}
3259
				}
3260
3261
				$to_close[] = $tag;
3262
			}
3263
			while ($tag['tag'] != $look_for);
3264
3265
			// Did we just eat through everything and not find it?
3266
			if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
3267
			{
3268
				$open_tags = $to_close;
3269
				continue;
3270
			}
3271
			elseif (!empty($to_close) && $tag['tag'] != $look_for)
3272
			{
3273
				if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]]))
3274
				{
3275
					foreach ($bbc_codes[$look_for[0]] as $temp)
3276
						if ($temp['tag'] == $look_for)
3277
						{
3278
							$block_level = !empty($temp['block_level']);
3279
							break;
3280
						}
3281
				}
3282
3283
				// We're not looking for a block level tag (or maybe even a tag that exists...)
3284
				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...
3285
				{
3286
					foreach ($to_close as $tag)
3287
						array_push($open_tags, $tag);
3288
					continue;
3289
				}
3290
			}
3291
3292
			foreach ($to_close as $tag)
3293
			{
3294
				$message = substr($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1);
3295
				$pos += strlen($tag['after']) + 2;
3296
				$pos2 = $pos - 1;
3297
3298
				// See the comment at the end of the big loop - just eating whitespace ;).
3299
				$whitespace_regex = '';
3300
				if (!empty($tag['block_level']))
3301
					$whitespace_regex .= '(&nbsp;|\s)*(<br\s*/?' . '>)?';
3302
				// Trim one line of whitespace after unnested tags, but all of it after nested ones
3303
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
3304
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
3305
3306
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
3307
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
3308
			}
3309
3310
			if (!empty($to_close))
3311
			{
3312
				$to_close = array();
0 ignored issues
show
The assignment to $to_close is dead and can be removed.
Loading history...
3313
				$pos--;
3314
			}
3315
3316
			continue;
3317
		}
3318
3319
		// No tags for this character, so just keep going (fastest possible course.)
3320
		if (!isset($bbc_codes[$tag_character]))
3321
			continue;
3322
3323
		$inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
3324
		$tag = null;
3325
		foreach ($bbc_codes[$tag_character] as $possible)
3326
		{
3327
			$pt_strlen = strlen($possible['tag']);
3328
3329
			// Not a match?
3330
			if (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag'])
3331
				continue;
3332
3333
			$next_c = isset($message[$pos + 1 + $pt_strlen]) ? $message[$pos + 1 + $pt_strlen] : '';
3334
3335
			// A tag is the last char maybe
3336
			if ($next_c == '')
3337
				break;
3338
3339
			// A test validation?
3340
			if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0)
3341
				continue;
3342
			// Do we want parameters?
3343
			elseif (!empty($possible['parameters']))
3344
			{
3345
				// Are all the parameters optional?
3346
				$param_required = false;
3347
				foreach ($possible['parameters'] as $param)
3348
				{
3349
					if (empty($param['optional']))
3350
					{
3351
						$param_required = true;
3352
						break;
3353
					}
3354
				}
3355
3356
				if ($param_required && $next_c != ' ')
3357
					continue;
3358
			}
3359
			elseif (isset($possible['type']))
3360
			{
3361
				// Do we need an equal sign?
3362
				if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=')
3363
					continue;
3364
				// Maybe we just want a /...
3365
				if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + $pt_strlen, 2) != '/]' && substr($message, $pos + 1 + $pt_strlen, 3) != ' /]')
3366
					continue;
3367
				// An immediate ]?
3368
				if ($possible['type'] == 'unparsed_content' && $next_c != ']')
3369
					continue;
3370
			}
3371
			// No type means 'parsed_content', which demands an immediate ] without parameters!
3372
			elseif ($next_c != ']')
3373
				continue;
3374
3375
			// Check allowed tree?
3376
			if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
3377
				continue;
3378
			elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
3379
				continue;
3380
			// If this is in the list of disallowed child tags, don't parse it.
3381
			elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
3382
				continue;
3383
3384
			$pos1 = $pos + 1 + $pt_strlen + 1;
3385
3386
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
3387
			if ($possible['tag'] == 'quote')
3388
			{
3389
				// Start with standard
3390
				$quote_alt = false;
3391
				foreach ($open_tags as $open_quote)
3392
				{
3393
					// Every parent quote this quote has flips the styling
3394
					if ($open_quote['tag'] == 'quote')
3395
						$quote_alt = !$quote_alt;
0 ignored issues
show
The condition $quote_alt is always false.
Loading history...
3396
				}
3397
				// Add a class to the quote to style alternating blockquotes
3398
				$possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">'));
3399
			}
3400
3401
			// This is long, but it makes things much easier and cleaner.
3402
			if (!empty($possible['parameters']))
3403
			{
3404
				// Build a regular expression for each parameter for the current tag.
3405
				$regex_key = $smcFunc['json_encode']($possible['parameters']);
3406
				if (!isset($params_regexes[$regex_key]))
3407
				{
3408
					$params_regexes[$regex_key] = '';
3409
3410
					foreach ($possible['parameters'] as $p => $info)
3411
						$params_regexes[$regex_key] .= '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . '\s*)' . (empty($info['optional']) ? '' : '?');
3412
				}
3413
3414
				// Extract the string that potentially holds our parameters.
3415
				$blob = preg_split('~\[/?(?:' . $alltags_regex . ')~i', substr($message, $pos));
3416
				$blobs = preg_split('~\]~i', $blob[1]);
3417
3418
				$splitters = implode('=|', array_keys($possible['parameters'])) . '=';
3419
3420
				// Progressively append more blobs until we find our parameters or run out of blobs
3421
				$blob_counter = 1;
3422
				while ($blob_counter <= count($blobs))
3423
				{
3424
					$given_param_string = implode(']', array_slice($blobs, 0, $blob_counter++));
3425
3426
					$given_params = preg_split('~\s(?=(' . $splitters . '))~i', $given_param_string);
3427
					sort($given_params, SORT_STRING);
3428
3429
					$match = preg_match('~^' . $params_regexes[$regex_key] . '$~i', implode(' ', $given_params), $matches) !== 0;
3430
3431
					if ($match)
3432
						break;
3433
				}
3434
3435
				// Didn't match our parameter list, try the next possible.
3436
				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...
3437
					continue;
3438
3439
				$params = array();
3440
				for ($i = 1, $n = count($matches); $i < $n; $i += 2)
3441
				{
3442
					$key = strtok(ltrim($matches[$i]), '=');
3443
					if ($key === false)
3444
						continue;
3445
					elseif (isset($possible['parameters'][$key]['value']))
3446
						$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
3447
					elseif (isset($possible['parameters'][$key]['validate']))
3448
						$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
3449
					else
3450
						$params['{' . $key . '}'] = $matches[$i + 1];
3451
3452
					// Just to make sure: replace any $ or { so they can't interpolate wrongly.
3453
					$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
3454
				}
3455
3456
				foreach ($possible['parameters'] as $p => $info)
3457
				{
3458
					if (!isset($params['{' . $p . '}']))
3459
					{
3460
						if (!isset($info['default']))
3461
							$params['{' . $p . '}'] = '';
3462
						elseif (isset($possible['parameters'][$p]['value']))
3463
							$params['{' . $p . '}'] = strtr($possible['parameters'][$p]['value'], array('$1' => $info['default']));
3464
						elseif (isset($possible['parameters'][$p]['validate']))
3465
							$params['{' . $p . '}'] = $possible['parameters'][$p]['validate']($info['default']);
3466
						else
3467
							$params['{' . $p . '}'] = $info['default'];
3468
					}
3469
				}
3470
3471
				$tag = $possible;
3472
3473
				// Put the parameters into the string.
3474
				if (isset($tag['before']))
3475
					$tag['before'] = strtr($tag['before'], $params);
3476
				if (isset($tag['after']))
3477
					$tag['after'] = strtr($tag['after'], $params);
3478
				if (isset($tag['content']))
3479
					$tag['content'] = strtr($tag['content'], $params);
3480
3481
				$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...
3482
			}
3483
			else
3484
			{
3485
				$tag = $possible;
3486
				$params = array();
3487
			}
3488
			break;
3489
		}
3490
3491
		// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
3492
		if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]], $message[$pos + 2]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
3493
		{
3494
			if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
3495
				continue;
3496
3497
			$tag = $itemcodes[$message[$pos + 1]];
3498
3499
			// First let's set up the tree: it needs to be in a list, or after an li.
3500
			if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
3501
			{
3502
				$open_tags[] = array(
3503
					'tag' => 'list',
3504
					'after' => '</ul>',
3505
					'block_level' => true,
3506
					'require_children' => array('li'),
3507
					'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
3508
				);
3509
				$code = '<ul class="bbc_list">';
3510
			}
3511
			// We're in a list item already: another itemcode?  Close it first.
3512
			elseif ($inside['tag'] == 'li')
3513
			{
3514
				array_pop($open_tags);
3515
				$code = '</li>';
3516
			}
3517
			else
3518
				$code = '';
3519
3520
			// Now we open a new tag.
3521
			$open_tags[] = array(
3522
				'tag' => 'li',
3523
				'after' => '</li>',
3524
				'trim' => 'outside',
3525
				'block_level' => true,
3526
				'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
3527
			);
3528
3529
			// First, open the tag...
3530
			$code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
3531
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
3532
			$pos += strlen($code) - 1 + 2;
3533
3534
			// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
3535
			$pos2 = strpos($message, '<br>', $pos);
3536
			$pos3 = strpos($message, '[/', $pos);
3537
			if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
3538
			{
3539
				preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 4), $matches);
3540
				$message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
3541
3542
				$open_tags[count($open_tags) - 2]['after'] = '</ul>';
3543
			}
3544
			// Tell the [list] that it needs to close specially.
3545
			else
3546
			{
3547
				// Move the li over, because we're not sure what we'll hit.
3548
				$open_tags[count($open_tags) - 1]['after'] = '';
3549
				$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
3550
			}
3551
3552
			continue;
3553
		}
3554
3555
		// Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
3556
		if ($tag === null && $inside !== null && !empty($inside['require_children']))
3557
		{
3558
			array_pop($open_tags);
3559
3560
			$message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
3561
			$pos += strlen($inside['after']) - 1 + 2;
3562
		}
3563
3564
		// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
3565
		if ($tag === null)
3566
			continue;
3567
3568
		// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
3569
		if (isset($inside['disallow_children']))
3570
			$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
3571
3572
		// Is this tag disabled?
3573
		if (isset($disabled[$tag['tag']]))
3574
		{
3575
			if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
3576
			{
3577
				$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
3578
				$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
3579
				$tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
3580
			}
3581
			elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
3582
			{
3583
				$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
3584
				$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
3585
			}
3586
			else
3587
				$tag['content'] = $tag['disabled_content'];
3588
		}
3589
3590
		// we use this a lot
3591
		$tag_strlen = strlen($tag['tag']);
3592
3593
		// The only special case is 'html', which doesn't need to close things.
3594
		if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
3595
		{
3596
			$n = count($open_tags) - 1;
3597
			while (empty($open_tags[$n]['block_level']) && $n >= 0)
3598
				$n--;
3599
3600
			// Close all the non block level tags so this tag isn't surrounded by them.
3601
			for ($i = count($open_tags) - 1; $i > $n; $i--)
3602
			{
3603
				$message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
3604
				$ot_strlen = strlen($open_tags[$i]['after']);
3605
				$pos += $ot_strlen + 2;
3606
				$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...
3607
3608
				// Trim or eat trailing stuff... see comment at the end of the big loop.
3609
				$whitespace_regex = '';
3610
				if (!empty($tag['block_level']))
3611
					$whitespace_regex .= '(&nbsp;|\s)*(<br>)?';
3612
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
3613
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
3614
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
3615
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
3616
3617
				array_pop($open_tags);
3618
			}
3619
		}
3620
3621
		// Can't read past the end of the message
3622
		$pos1 = min(strlen($message), $pos1);
3623
3624
		// No type means 'parsed_content'.
3625
		if (!isset($tag['type']))
3626
		{
3627
			$open_tags[] = $tag;
3628
3629
			// There's no data to change, but maybe do something based on params?
3630
			$data = null;
3631
			if (isset($tag['validate']))
3632
				$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...
3633
3634
			$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
3635
			$pos += strlen($tag['before']) - 1 + 2;
3636
		}
3637
		// Don't parse the content, just skip it.
3638
		elseif ($tag['type'] == 'unparsed_content')
3639
		{
3640
			$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
3641
			if ($pos2 === false)
3642
				continue;
3643
3644
			$data = substr($message, $pos1, $pos2 - $pos1);
3645
3646
			if (!empty($tag['block_level']) && substr($data, 0, 4) == '<br>')
3647
				$data = substr($data, 4);
3648
3649
			if (isset($tag['validate']))
3650
				$tag['validate']($tag, $data, $disabled, $params);
3651
3652
			$code = strtr($tag['content'], array('$1' => $data));
3653
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
3654
3655
			$pos += strlen($code) - 1 + 2;
3656
			$last_pos = $pos + 1;
3657
		}
3658
		// Don't parse the content, just skip it.
3659
		elseif ($tag['type'] == 'unparsed_equals_content')
3660
		{
3661
			// The value may be quoted for some tags - check.
3662
			if (isset($tag['quoted']))
3663
			{
3664
				// Anything passed through the preparser will use &quot;,
3665
				// but we need to handle raw quotation marks too.
3666
				$quot = substr($message, $pos1, 1) === '"' ? '"' : '&quot;';
3667
3668
				$quoted = substr($message, $pos1, strlen($quot)) == $quot;
3669
				if ($tag['quoted'] != 'optional' && !$quoted)
3670
					continue;
3671
3672
				if ($quoted)
3673
					$pos1 += strlen($quot);
3674
			}
3675
			else
3676
				$quoted = false;
3677
3678
			$pos2 = strpos($message, $quoted == false ? ']' : $quot . ']', $pos1);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $quot does not seem to be defined for all execution paths leading up to this point.
Loading history...
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...
3679
			if ($pos2 === false)
3680
				continue;
3681
3682
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3683
			if ($pos3 === false)
3684
				continue;
3685
3686
			$data = array(
3687
				substr($message, $pos2 + ($quoted == false ? 1 : 1 + strlen($quot)), $pos3 - ($pos2 + ($quoted == false ? 1 : 1 + strlen($quot)))),
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...
3688
				substr($message, $pos1, $pos2 - $pos1)
3689
			);
3690
3691
			if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '<br>')
3692
				$data[0] = substr($data[0], 4);
3693
3694
			// Validation for my parking, please!
3695
			if (isset($tag['validate']))
3696
				$tag['validate']($tag, $data, $disabled, $params);
3697
3698
			$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
3699
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3700
			$pos += strlen($code) - 1 + 2;
3701
		}
3702
		// A closed tag, with no content or value.
3703
		elseif ($tag['type'] == 'closed')
3704
		{
3705
			$pos2 = strpos($message, ']', $pos);
3706
3707
			// Maybe a custom BBC wants to do something special?
3708
			$data = null;
3709
			if (isset($tag['validate']))
3710
				$tag['validate']($tag, $data, $disabled, $params);
3711
3712
			$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
3713
			$pos += strlen($tag['content']) - 1 + 2;
3714
		}
3715
		// This one is sorta ugly... :/.  Unfortunately, it's needed for flash.
3716
		elseif ($tag['type'] == 'unparsed_commas_content')
3717
		{
3718
			$pos2 = strpos($message, ']', $pos1);
3719
			if ($pos2 === false)
3720
				continue;
3721
3722
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3723
			if ($pos3 === false)
3724
				continue;
3725
3726
			// We want $1 to be the content, and the rest to be csv.
3727
			$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
3728
			$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
3729
3730
			if (isset($tag['validate']))
3731
				$tag['validate']($tag, $data, $disabled, $params);
3732
3733
			$code = $tag['content'];
3734
			foreach ($data as $k => $d)
3735
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3736
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3737
			$pos += strlen($code) - 1 + 2;
3738
		}
3739
		// This has parsed content, and a csv value which is unparsed.
3740
		elseif ($tag['type'] == 'unparsed_commas')
3741
		{
3742
			$pos2 = strpos($message, ']', $pos1);
3743
			if ($pos2 === false)
3744
				continue;
3745
3746
			$data = explode(',', substr($message, $pos1, $pos2 - $pos1));
3747
3748
			if (isset($tag['validate']))
3749
				$tag['validate']($tag, $data, $disabled, $params);
3750
3751
			// Fix after, for disabled code mainly.
3752
			foreach ($data as $k => $d)
3753
				$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
3754
3755
			$open_tags[] = $tag;
3756
3757
			// Replace them out, $1, $2, $3, $4, etc.
3758
			$code = $tag['before'];
3759
			foreach ($data as $k => $d)
3760
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3761
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1);
3762
			$pos += strlen($code) - 1 + 2;
3763
		}
3764
		// A tag set to a value, parsed or not.
3765
		elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
3766
		{
3767
			// The value may be quoted for some tags - check.
3768
			if (isset($tag['quoted']))
3769
			{
3770
				// Will normally be '&quot;' but might be '"'.
3771
				$quot = substr($message, $pos1, 1) === '"' ? '"' : '&quot;';
3772
3773
				$quoted = substr($message, $pos1, strlen($quot)) == $quot;
3774
				if ($tag['quoted'] != 'optional' && !$quoted)
3775
					continue;
3776
3777
				if ($quoted)
3778
					$pos1 += strlen($quot);
3779
			}
3780
			else
3781
				$quoted = false;
3782
3783
			if ($quoted)
3784
			{
3785
				$end_of_value = strpos($message, $quot . ']', $pos1);
3786
				$nested_tag = strpos($message, '=' . $quot, $pos1);
3787
				// Check so this is not just an quoted url ending with a =
3788
				if ($nested_tag && substr($message, $nested_tag, 2 + strlen($quot)) == '=' . $quot . ']')
3789
					$nested_tag = false;
3790
				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...
3791
					// Nested tag with quoted value detected, use next end tag
3792
					$nested_tag_pos = strpos($message, $quoted == false ? ']' : $quot, $pos1) + strlen($quot);
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...
3793
			}
3794
3795
			$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...
3796
			if ($pos2 === false)
3797
				continue;
3798
3799
			$data = substr($message, $pos1, $pos2 - $pos1);
3800
3801
			// Validation for my parking, please!
3802
			if (isset($tag['validate']))
3803
				$tag['validate']($tag, $data, $disabled, $params);
3804
3805
			// For parsed content, we must recurse to avoid security problems.
3806
			if ($tag['type'] != 'unparsed_equals')
3807
				$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
3808
3809
			$tag['after'] = strtr($tag['after'], array('$1' => $data));
3810
3811
			$open_tags[] = $tag;
3812
3813
			$code = strtr($tag['before'], array('$1' => $data));
3814
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + ($quoted == false ? 1 : 1 + strlen($quot)));
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...
3815
			$pos += strlen($code) - 1 + 2;
3816
		}
3817
3818
		// If this is block level, eat any breaks after it.
3819
		if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '<br>')
3820
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 5);
3821
3822
		// Are we trimming outside this tag?
3823
		if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
3824
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
3825
	}
3826
3827
	// Close any remaining tags.
3828
	while ($tag = array_pop($open_tags))
3829
		$message .= "\n" . $tag['after'] . "\n";
3830
3831
	// Parse the smileys within the parts where it can be done safely.
3832
	if ($smileys === true)
3833
	{
3834
		$message_parts = explode("\n", $message);
3835
		for ($i = 0, $n = count($message_parts); $i < $n; $i += 2)
3836
			parsesmileys($message_parts[$i]);
3837
3838
		$message = implode('', $message_parts);
3839
	}
3840
3841
	// No smileys, just get rid of the markers.
3842
	else
3843
		$message = strtr($message, array("\n" => ''));
3844
3845
	if ($message !== '' && $message[0] === ' ')
3846
		$message = '&nbsp;' . substr($message, 1);
3847
3848
	// Cleanup whitespace.
3849
	$message = strtr($message, array('  ' => ' &nbsp;', "\r" => '', "\n" => '<br>', '<br> ' => '<br>&nbsp;', '&#13;' => "\n"));
3850
3851
	// Allow mods access to what parse_bbc created
3852
	call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
3853
3854
	// Cache the output if it took some time...
3855
	if (isset($cache_key, $cache_t) && microtime(true) - $cache_t > 0.05)
3856
		cache_put_data($cache_key, $message, 240);
3857
3858
	// If this was a force parse revert if needed.
3859
	if (!empty($parse_tags))
3860
	{
3861
		$alltags_regex = empty($real_alltags_regex) ? '' : $real_alltags_regex;
3862
		unset($real_alltags_regex);
3863
	}
3864
	elseif (!empty($bbc_codes))
3865
		$bbc_lang_locales[$txt['lang_locale']] = $bbc_codes;
3866
3867
	return $message;
3868
}
3869
3870
/**
3871
 * Parse smileys in the passed message.
3872
 *
3873
 * The smiley parsing function which makes pretty faces appear :).
3874
 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
3875
 * These are specifically not parsed in code tags [url=mailto:[email protected]]
3876
 * Caches the smileys from the database or array in memory.
3877
 * Doesn't return anything, but rather modifies message directly.
3878
 *
3879
 * @param string &$message The message to parse smileys in
3880
 */
3881
function parsesmileys(&$message)
3882
{
3883
	global $modSettings, $txt, $user_info, $context, $smcFunc;
3884
	static $smileyPregSearch = null, $smileyPregReplacements = array();
3885
3886
	// No smiley set at all?!
3887
	if ($user_info['smiley_set'] == 'none' || trim($message) == '')
3888
		return;
3889
3890
	// Maybe a mod wants to implement an alternative method (e.g. emojis instead of images)
3891
	call_integration_hook('integrate_smileys', array(&$smileyPregSearch, &$smileyPregReplacements));
3892
3893
	// If smileyPregSearch hasn't been set, do it now.
3894
	if (empty($smileyPregSearch))
3895
	{
3896
		// Cache for longer when customized smiley codes aren't enabled
3897
		$cache_time = empty($modSettings['smiley_enable']) ? 7200 : 480;
3898
3899
		// Load the smileys in reverse order by length so they don't get parsed incorrectly.
3900
		if (($temp = cache_get_data('parsing_smileys_' . $user_info['smiley_set'], $cache_time)) == null)
3901
		{
3902
			$result = $smcFunc['db_query']('', '
3903
				SELECT s.code, f.filename, s.description
3904
				FROM {db_prefix}smileys AS s
3905
					JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
3906
				WHERE f.smiley_set = {string:smiley_set}' . (empty($modSettings['smiley_enable']) ? '
3907
					AND s.code IN ({array_string:default_codes})' : '') . '
3908
				ORDER BY LENGTH(s.code) DESC',
3909
				array(
3910
					'default_codes' => array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'),
3911
					'smiley_set' => $user_info['smiley_set'],
3912
				)
3913
			);
3914
			$smileysfrom = array();
3915
			$smileysto = array();
3916
			$smileysdescs = array();
3917
			while ($row = $smcFunc['db_fetch_assoc']($result))
3918
			{
3919
				$smileysfrom[] = $row['code'];
3920
				$smileysto[] = $smcFunc['htmlspecialchars']($row['filename']);
3921
				$smileysdescs[] = !empty($txt['icon_' . strtolower($row['description'])]) ? $txt['icon_' . strtolower($row['description'])] : $row['description'];
3922
			}
3923
			$smcFunc['db_free_result']($result);
3924
3925
			cache_put_data('parsing_smileys_' . $user_info['smiley_set'], array($smileysfrom, $smileysto, $smileysdescs), $cache_time);
3926
		}
3927
		else
3928
			list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
3929
3930
		// The non-breaking-space is a complex thing...
3931
		$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
3932
3933
		// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:[email protected]] doesn't parse the :D smiley)
3934
		$smileyPregReplacements = array();
3935
		$searchParts = array();
3936
		$smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
3937
3938
		for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
3939
		{
3940
			$specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES);
3941
			$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">';
3942
3943
			$smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
3944
3945
			$searchParts[] = $smileysfrom[$i];
3946
			if ($smileysfrom[$i] != $specialChars)
3947
			{
3948
				$smileyPregReplacements[$specialChars] = $smileyCode;
3949
				$searchParts[] = $specialChars;
3950
3951
				// Some 2.0 hex htmlchars are in there as 3 digits; allow for finding leading 0 or not
3952
				$specialChars2 = preg_replace('/&#(\d{2});/', '&#0$1;', $specialChars);
3953
				if ($specialChars2 != $specialChars)
3954
				{
3955
					$smileyPregReplacements[$specialChars2] = $smileyCode;
3956
					$searchParts[] = $specialChars2;
3957
				}
3958
			}
3959
		}
3960
3961
		$smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?<![a-zA-Z0-9])\(|^)(' . build_regex($searchParts, '~') . ')(?=[^[:alpha:]0-9]|$)~' . ($context['utf8'] ? 'u' : '');
0 ignored issues
show
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

3961
		$smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?<![a-zA-Z0-9])\(|^)(' . /** @scrutinizer ignore-type */ build_regex($searchParts, '~') . ')(?=[^[:alpha:]0-9]|$)~' . ($context['utf8'] ? 'u' : '');
Loading history...
3962
	}
3963
3964
	// If there are no smileys defined, no need to replace anything
3965
	if (empty($smileyPregReplacements))
3966
		return;
3967
3968
	// Replace away!
3969
	$message = preg_replace_callback(
3970
		$smileyPregSearch,
3971
		function($matches) use ($smileyPregReplacements)
3972
		{
3973
			return $smileyPregReplacements[$matches[1]];
3974
		},
3975
		$message
3976
	);
3977
}
3978
3979
/**
3980
 * Highlight any code.
3981
 *
3982
 * Uses PHP's highlight_string() to highlight PHP syntax
3983
 * does special handling to keep the tabs in the code available.
3984
 * used to parse PHP code from inside [code] and [php] tags.
3985
 *
3986
 * @param string $code The code
3987
 * @return string The code with highlighted HTML.
3988
 */
3989
function highlight_php_code($code)
3990
{
3991
	// Remove special characters.
3992
	$code = un_htmlspecialchars(strtr($code, array('<br />' => "\n", '<br>' => "\n", "\t" => 'SMF_TAB();', '&#91;' => '[')));
3993
3994
	$oldlevel = error_reporting(0);
3995
3996
	$buffer = str_replace(array("\n", "\r"), array('<br />', ''), @highlight_string($code, true));
3997
3998
	error_reporting($oldlevel);
3999
4000
	// Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
4001
	$buffer = preg_replace('~SMF_TAB(?:</(?:font|span)><(?:font color|span style)="[^"]*?">)?\(\);~', '<span style="white-space: pre-wrap;">' . "\t" . '</span>', $buffer);
4002
4003
	// PHP 8.3 changed the returned HTML.
4004
	$buffer = preg_replace('/^(<pre>)?<code[^>]*>|<\/code>(<\/pre>)?$/', '', $buffer);
4005
4006
	// Remove line breaks inserted before & after the actual code in php < 8.3
4007
	$buffer = preg_replace('/^(<span\s[^>]*>)<br \/>/', '$1', $buffer);
4008
	$buffer = preg_replace('/<br \/>(<\/span[^>]*>)<br \/>$/', '$1', $buffer);
4009
4010
	return strtr($buffer, ['\'' => '&#039;']);
4011
}
4012
4013
/**
4014
 * Gets the appropriate URL to use for images (or whatever) when using SSL
4015
 *
4016
 * The returned URL may or may not be a proxied URL, depending on the situation.
4017
 * Mods can implement alternative proxies using the 'integrate_proxy' hook.
4018
 *
4019
 * @param string $url The original URL of the requested resource
4020
 * @return string The URL to use
4021
 */
4022
function get_proxied_url($url)
4023
{
4024
	global $boardurl, $image_proxy_enabled, $image_proxy_secret, $user_info;
4025
4026
	// Only use the proxy if enabled, and never for robots
4027
	if (empty($image_proxy_enabled) || !empty($user_info['possibly_robot']))
4028
		return $url;
4029
4030
	$parsedurl = parse_iri($url);
4031
4032
	// Don't bother with HTTPS URLs, schemeless URLs, or obviously invalid URLs
4033
	if (empty($parsedurl['scheme']) || empty($parsedurl['host']) || empty($parsedurl['path']) || $parsedurl['scheme'] === 'https')
4034
		return $url;
4035
4036
	// We don't need to proxy our own resources
4037
	if ($parsedurl['host'] === parse_iri($boardurl, PHP_URL_HOST))
4038
		return strtr($url, array('http://' => 'https://'));
4039
4040
	// By default, use SMF's own image proxy script
4041
	$proxied_url = strtr($boardurl, array('http://' => 'https://')) . '/proxy.php?request=' . urlencode($url) . '&hash=' . hash_hmac('sha1', $url, $image_proxy_secret);
4042
4043
	// Allow mods to easily implement an alternative proxy
4044
	// MOD AUTHORS: To add settings UI for your proxy, use the integrate_general_settings hook.
4045
	call_integration_hook('integrate_proxy', array($url, &$proxied_url));
4046
4047
	return $proxied_url;
4048
}
4049
4050
/**
4051
 * Make sure the browser doesn't come back and repost the form data.
4052
 * Should be used whenever anything is posted.
4053
 *
4054
 * @param string $setLocation The URL to redirect them to
4055
 * @param bool $refresh Whether to use a meta refresh instead
4056
 * @param bool $permanent Whether to send a 301 Moved Permanently instead of a 302 Moved Temporarily
4057
 */
4058
function redirectexit($setLocation = '', $refresh = false, $permanent = false)
4059
{
4060
	global $scripturl, $context, $modSettings, $db_show_debug, $db_cache;
4061
4062
	// In case we have mail to send, better do that - as obExit doesn't always quite make it...
4063
	if (!empty($context['flush_mail']))
4064
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
4065
		AddMailQueue(true);
4066
4067
	$add = preg_match('~^(ftp|http)[s]?://~', $setLocation) == 0 && substr($setLocation, 0, 6) != 'about:';
4068
4069
	if ($add)
4070
		$setLocation = $scripturl . ($setLocation != '' ? '?' . $setLocation : '');
4071
4072
	// PHP 8.4 deprecated SID. A better long-term solution is needed, but this works for now.
4073
	$sid = defined('SID') ? @constant('SID') : null;
4074
4075
	// Put the session ID in.
4076
	if (isset($sid) && $sid != '')
4077
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote($sid, '/') . ')\\??/', $scripturl . '?' . $sid . ';', $setLocation);
4078
	// Keep that debug in their for template debugging!
4079
	elseif (isset($_GET['debug']))
4080
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);
4081
4082
	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'])))
4083
	{
4084
		if (isset($sid) && $sid != '')
4085
			$setLocation = preg_replace_callback(
4086
				'~^' . preg_quote($scripturl, '~') . '\?(?:' . $sid . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~',
4087
				function($m) use ($scripturl, $sid)
4088
				{
4089
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . $sid . (isset($m[2]) ? "$m[2]" : "");
4090
				},
4091
				$setLocation
4092
			);
4093
		else
4094
			$setLocation = preg_replace_callback(
4095
				'~^' . preg_quote($scripturl, '~') . '\?((?:board|topic)=[^#"]+?)(#[^"]*?)?$~',
4096
				function($m) use ($scripturl)
4097
				{
4098
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html' . (isset($m[2]) ? "$m[2]" : "");
4099
				},
4100
				$setLocation
4101
			);
4102
	}
4103
4104
	// The request was from ajax/xhr/other api call, append ajax ot the url.
4105
	if (!empty($context['from_ajax']))
4106
		$setLocation .= (strpos($setLocation, '?') ? ';' : '?') . 'ajax';
4107
4108
	// Maybe integrations want to change where we are heading?
4109
	call_integration_hook('integrate_redirect', array(&$setLocation, &$refresh, &$permanent));
4110
4111
	// Set the header.
4112
	header('location: ' . str_replace(' ', '%20', $setLocation), true, $permanent ? 301 : 302);
4113
4114
	// Debugging.
4115
	if (isset($db_show_debug) && $db_show_debug === true)
4116
		$_SESSION['debug_redirect'] = $db_cache;
4117
4118
	obExit(false);
4119
}
4120
4121
/**
4122
 * Ends execution.  Takes care of template loading and remembering the previous URL.
4123
 *
4124
 * @param bool $header Whether to do the header
4125
 * @param bool $do_footer Whether to do the footer
4126
 * @param bool $from_index Whether we're coming from the board index
4127
 * @param bool $from_fatal_error Whether we're coming from a fatal error
4128
 */
4129
function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false)
4130
{
4131
	global $context, $settings, $modSettings, $txt, $smcFunc, $should_log;
4132
	static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
4133
4134
	// Attempt to prevent a recursive loop.
4135
	++$level;
4136
	if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
4137
		exit;
0 ignored issues
show
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...
4138
	if ($from_fatal_error)
4139
		$has_fatal_error = true;
4140
4141
	// Clear out the stat cache.
4142
	if (function_exists('trackStats'))
4143
		trackStats();
4144
4145
	// If we have mail to send, send it.
4146
	if (function_exists('AddMailQueue') && !empty($context['flush_mail']))
4147
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
4148
		AddMailQueue(true);
4149
4150
	$do_header = $header === null ? !$header_done : $header;
4151
	if ($do_footer === null)
4152
		$do_footer = $do_header;
4153
4154
	// Has the template/header been done yet?
4155
	if ($do_header)
4156
	{
4157
		// Was the page title set last minute? Also update the HTML safe one.
4158
		if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
4159
			$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](html_entity_decode($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
4160
4161
		// Start up the session URL fixer.
4162
		ob_start('ob_sessrewrite');
4163
4164
		if (!empty($settings['output_buffers']) && is_string($settings['output_buffers']))
4165
			$buffers = explode(',', $settings['output_buffers']);
4166
		elseif (!empty($settings['output_buffers']))
4167
			$buffers = $settings['output_buffers'];
4168
		else
4169
			$buffers = array();
4170
4171
		if (isset($modSettings['integrate_buffer']))
4172
			$buffers = array_merge(explode(',', $modSettings['integrate_buffer']), $buffers);
4173
4174
		if (!empty($buffers))
4175
			foreach ($buffers as $function)
4176
			{
4177
				$call = call_helper($function, true);
4178
4179
				// Is it valid?
4180
				if (!empty($call))
4181
					ob_start($call);
0 ignored issues
show
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

4181
					ob_start(/** @scrutinizer ignore-type */ $call);
Loading history...
4182
			}
4183
4184
		// Display the screen in the logical order.
4185
		template_header();
4186
		$header_done = true;
4187
	}
4188
	if ($do_footer)
4189
	{
4190
		loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main');
4191
4192
		// Anything special to put out?
4193
		if (!empty($context['insert_after_template']) && !isset($_REQUEST['xml']))
4194
			echo $context['insert_after_template'];
4195
4196
		// Just so we don't get caught in an endless loop of errors from the footer...
4197
		if (!$footer_done)
4198
		{
4199
			$footer_done = true;
4200
			template_footer();
4201
4202
			// (since this is just debugging... it's okay that it's after </html>.)
4203
			if (!isset($_REQUEST['xml']))
4204
				displayDebug();
4205
		}
4206
	}
4207
4208
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
4209
	if ($should_log && !isset($_REQUEST['xml']))
4210
		$_SESSION['old_url'] = $_SERVER['REQUEST_URL'];
4211
4212
	// For session check verification.... don't switch browsers...
4213
	$_SESSION['USER_AGENT'] = empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT'];
4214
4215
	// Hand off the output to the portal, etc. we're integrated with.
4216
	call_integration_hook('integrate_exit', array($do_footer));
4217
4218
	// Don't exit if we're coming from index.php; that will pass through normally.
4219
	if (!$from_index)
4220
		exit;
0 ignored issues
show
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...
4221
}
4222
4223
/**
4224
 * Get the size of a specified image with better error handling.
4225
 *
4226
 * @todo see if it's better in Subs-Graphics, but one step at the time.
4227
 * Uses getimagesize() to determine the size of a file.
4228
 * Attempts to connect to the server first so it won't time out.
4229
 *
4230
 * @param string $url The URL of the image
4231
 * @return array|false The image size as array (width, height), or false on failure
4232
 */
4233
function url_image_size($url)
4234
{
4235
	global $sourcedir;
4236
4237
	// Make sure it is a proper URL.
4238
	$url = str_replace(' ', '%20', $url);
4239
4240
	// Can we pull this from the cache... please please?
4241
	if (($temp = cache_get_data('url_image_size-' . md5($url), 240)) !== null)
4242
		return $temp;
4243
	$t = microtime(true);
4244
4245
	// Get the host to pester...
4246
	preg_match('~^\w+://(.+?)/(.*)$~', $url, $match);
4247
4248
	// Can't figure it out, just try the image size.
4249
	if ($url == '' || $url == 'http://' || $url == 'https://')
4250
	{
4251
		return false;
4252
	}
4253
	elseif (!isset($match[1]))
4254
	{
4255
		$size = @getimagesize($url);
4256
	}
4257
	else
4258
	{
4259
		// Try to connect to the server... give it half a second.
4260
		$temp = 0;
4261
		$fp = @fsockopen($match[1], 80, $temp, $temp, 0.5);
4262
4263
		// Successful?  Continue...
4264
		if ($fp != false)
4265
		{
4266
			// Send the HEAD request (since we don't have to worry about chunked, HTTP/1.1 is fine here.)
4267
			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");
4268
4269
			// Read in the HTTP/1.1 or whatever.
4270
			$test = substr(fgets($fp, 11), -1);
4271
			fclose($fp);
4272
4273
			// See if it returned a 404/403 or something.
4274
			if ($test < 4)
4275
			{
4276
				$size = @getimagesize($url);
4277
4278
				// This probably means allow_url_fopen is off, let's try GD.
4279
				if ($size === false && function_exists('imagecreatefromstring'))
4280
				{
4281
					// It's going to hate us for doing this, but another request...
4282
					$image = @imagecreatefromstring(fetch_web_data($url));
0 ignored issues
show
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

4282
					$image = @imagecreatefromstring(/** @scrutinizer ignore-type */ fetch_web_data($url));
Loading history...
4283
					if ($image !== false)
4284
					{
4285
						$size = array(imagesx($image), imagesy($image));
4286
						imagedestroy($image);
4287
					}
4288
				}
4289
			}
4290
		}
4291
	}
4292
4293
	// If we didn't get it, we failed.
4294
	if (!isset($size))
4295
		$size = false;
4296
4297
	// If this took a long time, we may never have to do it again, but then again we might...
4298
	if (microtime(true) - $t > 0.8)
4299
		cache_put_data('url_image_size-' . md5($url), $size, 240);
4300
4301
	// Didn't work.
4302
	return $size;
4303
}
4304
4305
/**
4306
 * Sets up the basic theme context stuff.
4307
 *
4308
 * @param bool $forceload Whether to load the theme even if it's already loaded
4309
 */
4310
function setupThemeContext($forceload = false)
4311
{
4312
	global $modSettings, $user_info, $scripturl, $context, $settings, $options, $txt, $maintenance;
4313
	global $smcFunc;
4314
	static $loaded = false;
4315
4316
	// Under SSI this function can be called more then once.  That can cause some problems.
4317
	//   So only run the function once unless we are forced to run it again.
4318
	if ($loaded && !$forceload)
4319
		return;
4320
4321
	$loaded = true;
4322
4323
	$context['in_maintenance'] = !empty($maintenance);
4324
	$context['current_time'] = timeformat(time(), false);
4325
	$context['current_action'] = isset($_GET['action']) ? $smcFunc['htmlspecialchars']($_GET['action']) : '';
4326
	$context['random_news_line'] = array();
4327
4328
	// Get some news...
4329
	$context['news_lines'] = array_filter(explode("\n", str_replace("\r", '', trim(addslashes($modSettings['news'])))));
4330
	for ($i = 0, $n = count($context['news_lines']); $i < $n; $i++)
4331
	{
4332
		if (trim($context['news_lines'][$i]) == '')
4333
			continue;
4334
4335
		// Clean it up for presentation ;).
4336
		$context['news_lines'][$i] = parse_bbc(stripslashes(trim($context['news_lines'][$i])), true, 'news' . $i);
4337
	}
4338
4339
	if (!empty($context['news_lines']) && (!empty($modSettings['allow_guestAccess']) || $context['user']['is_logged']))
4340
		$context['random_news_line'] = $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)];
4341
4342
	if (!$user_info['is_guest'])
4343
	{
4344
		$context['user']['messages'] = &$user_info['messages'];
4345
		$context['user']['unread_messages'] = &$user_info['unread_messages'];
4346
		$context['user']['alerts'] = &$user_info['alerts'];
4347
4348
		// Personal message popup...
4349
		if ($user_info['unread_messages'] > (isset($_SESSION['unread_messages']) ? $_SESSION['unread_messages'] : 0))
4350
			$context['user']['popup_messages'] = true;
4351
		else
4352
			$context['user']['popup_messages'] = false;
4353
		$_SESSION['unread_messages'] = $user_info['unread_messages'];
4354
4355
		if (allowedTo('moderate_forum'))
4356
			$context['unapproved_members'] = !empty($modSettings['unapprovedMembers']) ? $modSettings['unapprovedMembers'] : 0;
4357
4358
		$context['user']['avatar'] = set_avatar_data(array(
4359
			'filename' => $user_info['avatar']['filename'],
4360
			'avatar' => $user_info['avatar']['url'],
4361
			'email' => $user_info['email'],
4362
		));
4363
4364
		// Figure out how long they've been logged in.
4365
		$context['user']['total_time_logged_in'] = array(
4366
			'days' => floor($user_info['total_time_logged_in'] / 86400),
4367
			'hours' => floor(($user_info['total_time_logged_in'] % 86400) / 3600),
4368
			'minutes' => floor(($user_info['total_time_logged_in'] % 3600) / 60)
4369
		);
4370
	}
4371
	else
4372
	{
4373
		$context['user']['messages'] = 0;
4374
		$context['user']['unread_messages'] = 0;
4375
		$context['user']['avatar'] = array();
4376
		$context['user']['total_time_logged_in'] = array('days' => 0, 'hours' => 0, 'minutes' => 0);
4377
		$context['user']['popup_messages'] = false;
4378
4379
		// If we've upgraded recently, go easy on the passwords.
4380
		if (!empty($modSettings['disableHashTime']) && ($modSettings['disableHashTime'] == 1 || time() < $modSettings['disableHashTime']))
4381
			$context['disable_login_hashing'] = true;
4382
	}
4383
4384
	// Setup the main menu items.
4385
	setupMenuContext();
4386
4387
	// This is here because old index templates might still use it.
4388
	$context['show_news'] = !empty($settings['enable_news']);
4389
4390
	// This is done to allow theme authors to customize it as they want.
4391
	$context['show_pm_popup'] = $context['user']['popup_messages'] && !empty($options['popup_messages']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != 'pm');
4392
4393
	// 2.1+: Add the PM popup here instead. Theme authors can still override it simply by editing/removing the 'fPmPopup' in the array.
4394
	if ($context['show_pm_popup'])
4395
		addInlineJavaScript('
4396
		jQuery(document).ready(function($) {
4397
			new smc_Popup({
4398
				heading: ' . JavaScriptEscape($txt['show_personal_messages_heading']) . ',
4399
				content: ' . JavaScriptEscape(sprintf($txt['show_personal_messages'], $context['user']['unread_messages'], $scripturl . '?action=pm')) . ',
4400
				icon_class: \'main_icons mail_new\'
4401
			});
4402
		});');
4403
4404
	// Add a generic "Are you sure?" confirmation message.
4405
	addInlineJavaScript('
4406
	var smf_you_sure =' . JavaScriptEscape($txt['quickmod_confirm']) . ';');
4407
4408
	// Now add the capping code for avatars.
4409
	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')
4410
		addInlineCss('
4411
	img.avatar { max-width: ' . $modSettings['avatar_max_width_external'] . 'px !important; max-height: ' . $modSettings['avatar_max_height_external'] . 'px !important; }');
4412
4413
	// Add max image limits
4414
	if (!empty($modSettings['max_image_width']))
4415
		addInlineCss('
4416
	.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); }');
4417
4418
	if (!empty($modSettings['max_image_height']))
4419
		addInlineCss('
4420
	.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; }');
4421
4422
	// This looks weird, but it's because BoardIndex.php references the variable.
4423
	$context['common_stats']['latest_member'] = array(
4424
		'id' => $modSettings['latestMember'],
4425
		'name' => $modSettings['latestRealName'],
4426
		'href' => $scripturl . '?action=profile;u=' . $modSettings['latestMember'],
4427
		'link' => '<a href="' . $scripturl . '?action=profile;u=' . $modSettings['latestMember'] . '">' . $modSettings['latestRealName'] . '</a>',
4428
	);
4429
	$context['common_stats'] = array(
4430
		'total_posts' => comma_format($modSettings['totalMessages']),
4431
		'total_topics' => comma_format($modSettings['totalTopics']),
4432
		'total_members' => comma_format($modSettings['totalMembers']),
4433
		'latest_member' => $context['common_stats']['latest_member'],
4434
	);
4435
	$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']);
4436
4437
	if (empty($settings['theme_version']))
4438
		addJavaScriptVar('smf_scripturl', $scripturl);
4439
4440
	if (!isset($context['page_title']))
4441
		$context['page_title'] = '';
4442
4443
	// Set some specific vars.
4444
	$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](html_entity_decode($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
4445
	$context['meta_keywords'] = !empty($modSettings['meta_keywords']) ? $smcFunc['htmlspecialchars']($modSettings['meta_keywords']) : '';
4446
4447
	// Content related meta tags, including Open Graph
4448
	$context['meta_tags'][] = array('property' => 'og:site_name', 'content' => $context['forum_name']);
4449
	$context['meta_tags'][] = array('property' => 'og:title', 'content' => $context['page_title_html_safe']);
4450
4451
	if (!empty($context['meta_keywords']))
4452
		$context['meta_tags'][] = array('name' => 'keywords', 'content' => $context['meta_keywords']);
4453
4454
	if (!empty($context['canonical_url']))
4455
		$context['meta_tags'][] = array('property' => 'og:url', 'content' => $context['canonical_url']);
4456
4457
	if (!empty($settings['og_image']))
4458
		$context['meta_tags'][] = array('property' => 'og:image', 'content' => $settings['og_image']);
4459
4460
	if (!empty($context['meta_description']))
4461
	{
4462
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['meta_description']);
4463
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['meta_description']);
4464
	}
4465
	else
4466
	{
4467
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['page_title_html_safe']);
4468
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['page_title_html_safe']);
4469
	}
4470
4471
	call_integration_hook('integrate_theme_context');
4472
}
4473
4474
/**
4475
 * Helper function to set the system memory to a needed value
4476
 * - If the needed memory is greater than current, will attempt to get more
4477
 * - if in_use is set to true, will also try to take the current memory usage in to account
4478
 *
4479
 * @param string $needed The amount of memory to request, if needed, like 256M
4480
 * @param bool $in_use Set to true to account for current memory usage of the script
4481
 * @return boolean True if we have at least the needed memory
4482
 */
4483
function setMemoryLimit($needed, $in_use = false)
4484
{
4485
	// everything in bytes
4486
	$memory_current = memoryReturnBytes(ini_get('memory_limit'));
4487
	$memory_needed = memoryReturnBytes($needed);
4488
4489
	// should we account for how much is currently being used?
4490
	if ($in_use)
4491
		$memory_needed += function_exists('memory_get_usage') ? memory_get_usage() : (2 * 1048576);
4492
4493
	// if more is needed, request it
4494
	if ($memory_current < $memory_needed)
4495
	{
4496
		@ini_set('memory_limit', ceil($memory_needed / 1048576) . 'M');
4497
		$memory_current = memoryReturnBytes(ini_get('memory_limit'));
4498
	}
4499
4500
	$memory_current = max($memory_current, memoryReturnBytes(get_cfg_var('memory_limit')));
0 ignored issues
show
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

4500
	$memory_current = max($memory_current, memoryReturnBytes(/** @scrutinizer ignore-type */ get_cfg_var('memory_limit')));
Loading history...
4501
4502
	// return success or not
4503
	return (bool) ($memory_current >= $memory_needed);
4504
}
4505
4506
/**
4507
 * Helper function to convert memory string settings to bytes
4508
 *
4509
 * @param string $val The byte string, like 256M or 1G
4510
 * @return integer The string converted to a proper integer in bytes
4511
 */
4512
function memoryReturnBytes($val)
4513
{
4514
	if (is_integer($val))
0 ignored issues
show
The condition is_integer($val) is always false.
Loading history...
4515
		return $val;
4516
4517
	// Separate the number from the designator
4518
	$val = trim($val);
4519
	$num = intval(substr($val, 0, strlen($val) - 1));
4520
	$last = strtolower(substr($val, -1));
4521
4522
	// convert to bytes
4523
	switch ($last)
4524
	{
4525
		case 'g':
4526
			$num *= 1024;
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
4527
		case 'm':
4528
			$num *= 1024;
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
4529
		case 'k':
4530
			$num *= 1024;
4531
	}
4532
	return $num;
4533
}
4534
4535
/**
4536
 * The header template
4537
 */
4538
function template_header()
4539
{
4540
	global $txt, $modSettings, $context, $user_info, $boarddir, $cachedir, $cache_enable, $language;
4541
4542
	setupThemeContext();
4543
4544
	// Print stuff to prevent caching of pages (except on attachment errors, etc.)
4545
	if (empty($context['no_last_modified']))
4546
	{
4547
		header('expires: Mon, 26 Jul 1997 05:00:00 GMT');
4548
		header('last-modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
4549
4550
		// Are we debugging the template/html content?
4551
		if (!isset($_REQUEST['xml']) && isset($_GET['debug']) && !isBrowser('ie'))
4552
			header('content-type: application/xhtml+xml');
4553
		elseif (!isset($_REQUEST['xml']))
4554
			header('content-type: text/html; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
4555
	}
4556
4557
	header('content-type: text/' . (isset($_REQUEST['xml']) ? 'xml' : 'html') . '; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
4558
4559
	// We need to splice this in after the body layer, or after the main layer for older stuff.
4560
	if ($context['in_maintenance'] && $context['user']['is_admin'])
4561
	{
4562
		$position = array_search('body', $context['template_layers']);
4563
		if ($position === false)
4564
			$position = array_search('main', $context['template_layers']);
4565
4566
		if ($position !== false)
4567
		{
4568
			$before = array_slice($context['template_layers'], 0, $position + 1);
4569
			$after = array_slice($context['template_layers'], $position + 1);
4570
			$context['template_layers'] = array_merge($before, array('maint_warning'), $after);
4571
		}
4572
	}
4573
4574
	$checked_securityFiles = false;
4575
	$showed_banned = false;
4576
	foreach ($context['template_layers'] as $layer)
4577
	{
4578
		loadSubTemplate($layer . '_above', true);
4579
4580
		// May seem contrived, but this is done in case the body and main layer aren't there...
4581
		if (in_array($layer, array('body', 'main')) && allowedTo('admin_forum') && !$user_info['is_guest'] && !$checked_securityFiles)
4582
		{
4583
			$checked_securityFiles = true;
4584
4585
			$securityFiles = array('install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~');
4586
4587
			// Add your own files.
4588
			call_integration_hook('integrate_security_files', array(&$securityFiles));
4589
4590
			foreach ($securityFiles as $i => $securityFile)
4591
			{
4592
				if (!file_exists($boarddir . '/' . $securityFile))
4593
					unset($securityFiles[$i]);
4594
			}
4595
4596
			// We are already checking so many files...just few more doesn't make any difference! :P
4597
			if (!empty($modSettings['currentAttachmentUploadDir']))
4598
				$path = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
4599
4600
			else
4601
				$path = $modSettings['attachmentUploadDir'];
4602
4603
			secureDirectory($path, true);
4604
			secureDirectory($cachedir);
4605
4606
			// If agreement is enabled, at least the english version shall exist
4607
			if (!empty($modSettings['requireAgreement']))
4608
				$agreement = !file_exists($boarddir . '/agreement.txt');
4609
4610
			// If privacy policy is enabled, at least the default language version shall exist
4611
			if (!empty($modSettings['requirePolicyAgreement']))
4612
				$policy_agreement = empty($modSettings['policy_' . $language]);
4613
4614
			if (!empty($securityFiles) ||
4615
				(!empty($cache_enable) && !is_writable($cachedir)) ||
4616
				!empty($agreement) ||
4617
				!empty($policy_agreement) ||
4618
				!empty($context['auth_secret_missing']))
4619
			{
4620
				echo '
4621
		<div class="errorbox">
4622
			<p class="alert">!!</p>
4623
			<h3>', empty($securityFiles) && empty($context['auth_secret_missing']) ? $txt['generic_warning'] : $txt['security_risk'], '</h3>
4624
			<p>';
4625
4626
				foreach ($securityFiles as $securityFile)
4627
				{
4628
					echo '
4629
				', $txt['not_removed'], '<strong>', $securityFile, '</strong>!<br>';
4630
4631
					if ($securityFile == 'Settings.php~' || $securityFile == 'Settings_bak.php~')
4632
						echo '
4633
				', sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)), '<br>';
4634
				}
4635
4636
				if (!empty($cache_enable) && !is_writable($cachedir))
4637
					echo '
4638
				<strong>', $txt['cache_writable'], '</strong><br>';
4639
4640
				if (!empty($agreement))
4641
					echo '
4642
				<strong>', $txt['agreement_missing'], '</strong><br>';
4643
4644
				if (!empty($policy_agreement))
4645
					echo '
4646
				<strong>', $txt['policy_agreement_missing'], '</strong><br>';
4647
4648
				if (!empty($context['auth_secret_missing']))
4649
					echo '
4650
				<strong>', $txt['auth_secret_missing'], '</strong><br>';
4651
4652
				echo '
4653
			</p>
4654
		</div>';
4655
			}
4656
		}
4657
		// If the user is banned from posting inform them of it.
4658
		elseif (in_array($layer, array('main', 'body')) && isset($_SESSION['ban']['cannot_post']) && !$showed_banned)
4659
		{
4660
			$showed_banned = true;
4661
			echo '
4662
				<div class="windowbg alert" style="margin: 2ex; padding: 2ex; border: 2px dashed red;">
4663
					', sprintf($txt['you_are_post_banned'], $user_info['is_guest'] ? $txt['guest_title'] : $user_info['name']);
4664
4665
			if (!empty($_SESSION['ban']['cannot_post']['reason']))
4666
				echo '
4667
					<div style="padding-left: 4ex; padding-top: 1ex;">', $_SESSION['ban']['cannot_post']['reason'], '</div>';
4668
4669
			if (!empty($_SESSION['ban']['expire_time']))
4670
				echo '
4671
					<div>', sprintf($txt['your_ban_expires'], timeformat($_SESSION['ban']['expire_time'], false)), '</div>';
4672
			else
4673
				echo '
4674
					<div>', $txt['your_ban_expires_never'], '</div>';
4675
4676
			echo '
4677
				</div>';
4678
		}
4679
	}
4680
}
4681
4682
/**
4683
 * Show the copyright.
4684
 */
4685
function theme_copyright()
4686
{
4687
	global $forum_copyright, $scripturl;
4688
4689
	// Don't display copyright for things like SSI.
4690
	if (SMF !== 1)
0 ignored issues
show
The condition SMF !== 1 is always true.
Loading history...
4691
		return;
4692
4693
	// Put in the version...
4694
	printf($forum_copyright, SMF_FULL_VERSION, SMF_SOFTWARE_YEAR, $scripturl);
4695
}
4696
4697
/**
4698
 * The template footer
4699
 */
4700
function template_footer()
4701
{
4702
	global $context, $modSettings, $db_count;
4703
4704
	// Show the load time?  (only makes sense for the footer.)
4705
	$context['show_load_time'] = !empty($modSettings['timeLoadPageEnable']);
4706
	$context['load_time'] = round(microtime(true) - TIME_START, 3);
4707
	$context['load_queries'] = $db_count;
4708
4709
	if (!empty($context['template_layers']) && is_array($context['template_layers']))
4710
		foreach (array_reverse($context['template_layers']) as $layer)
4711
			loadSubTemplate($layer . '_below', true);
4712
}
4713
4714
/**
4715
 * Output the Javascript files
4716
 * 	- tabbing in this function is to make the HTML source look good and proper
4717
 *  - if deferred is set function will output all JS set to load at page end
4718
 *
4719
 * @param bool $do_deferred If true will only output the deferred JS (the stuff that goes right before the closing body tag)
4720
 */
4721
function template_javascript($do_deferred = false)
4722
{
4723
	global $context, $modSettings, $settings;
4724
4725
	// Use this hook to minify/optimize Javascript files and vars
4726
	call_integration_hook('integrate_pre_javascript_output', array(&$do_deferred));
4727
4728
	$toMinify = array(
4729
		'standard' => array(),
4730
		'defer' => array(),
4731
		'async' => array(),
4732
	);
4733
4734
	// Ouput the declared Javascript variables.
4735
	if (!empty($context['javascript_vars']) && !$do_deferred)
4736
	{
4737
		echo '
4738
	<script>';
4739
4740
		foreach ($context['javascript_vars'] as $key => $value)
4741
		{
4742
			if (!is_string($key) || is_numeric($key))
4743
				continue;
4744
4745
			if (!is_string($value) && !is_numeric($value))
4746
				$value = null;
4747
4748
			echo "\n\t\t", 'var ', $key, isset($value) ? ' = ' . $value : '', ';';
4749
		}
4750
4751
		echo '
4752
	</script>';
4753
	}
4754
4755
	// In the dark days before HTML5, deferred JS files needed to be loaded at the end of the body.
4756
	// Now we load them in the head and use 'async' and/or 'defer' attributes. Much better performance.
4757
	if (!$do_deferred)
4758
	{
4759
		// While we have JavaScript files to place in the template.
4760
		foreach ($context['javascript_files'] as $id => $js_file)
4761
		{
4762
			// Last minute call! allow theme authors to disable single files.
4763
			if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
4764
				continue;
4765
4766
			// By default files don't get minimized unless the file explicitly says so!
4767
			if (!empty($js_file['options']['minimize']) && !empty($modSettings['minimize_files']))
4768
			{
4769
				if (!empty($js_file['options']['async']))
4770
					$toMinify['async'][] = $js_file;
4771
4772
				elseif (!empty($js_file['options']['defer']))
4773
					$toMinify['defer'][] = $js_file;
4774
4775
				else
4776
					$toMinify['standard'][] = $js_file;
4777
4778
				// Grab a random seed.
4779
				if (!isset($minSeed) && isset($js_file['options']['seed']))
4780
					$minSeed = $js_file['options']['seed'];
4781
			}
4782
4783
			else
4784
			{
4785
				echo '
4786
	<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' : '';
4787
4788
				if (!empty($js_file['options']['attributes']))
4789
					foreach ($js_file['options']['attributes'] as $key => $value)
4790
					{
4791
						if (is_bool($value))
4792
							echo !empty($value) ? ' ' . $key : '';
4793
4794
						else
4795
							echo ' ', $key, '="', $value, '"';
4796
					}
4797
4798
				echo '></script>';
4799
			}
4800
		}
4801
4802
		foreach ($toMinify as $js_files)
4803
		{
4804
			if (!empty($js_files))
4805
			{
4806
				$result = custMinify($js_files, 'js');
4807
4808
				$minSuccessful = array_keys($result) === array('smf_minified');
4809
4810
				foreach ($result as $minFile)
4811
					echo '
4812
	<script src="', $minFile['fileUrl'], $minSuccessful && isset($minSeed) ? $minSeed : '', '"', !empty($minFile['options']['async']) ? ' async' : '', !empty($minFile['options']['defer']) ? ' defer' : '', '></script>';
4813
			}
4814
		}
4815
	}
4816
4817
	// Inline JavaScript - Actually useful some times!
4818
	if (!empty($context['javascript_inline']))
4819
	{
4820
		if (!empty($context['javascript_inline']['defer']) && $do_deferred)
4821
		{
4822
			echo '
4823
<script>
4824
window.addEventListener("DOMContentLoaded", function() {';
4825
4826
			foreach ($context['javascript_inline']['defer'] as $js_code)
4827
				echo $js_code;
4828
4829
			echo '
4830
});
4831
</script>';
4832
		}
4833
4834
		if (!empty($context['javascript_inline']['standard']) && !$do_deferred)
4835
		{
4836
			echo '
4837
	<script>';
4838
4839
			foreach ($context['javascript_inline']['standard'] as $js_code)
4840
				echo $js_code;
4841
4842
			echo '
4843
	</script>';
4844
		}
4845
	}
4846
}
4847
4848
/**
4849
 * Output the CSS files
4850
 */
4851
function template_css()
4852
{
4853
	global $context, $db_show_debug, $boardurl, $settings, $modSettings;
4854
4855
	// Use this hook to minify/optimize CSS files
4856
	call_integration_hook('integrate_pre_css_output');
4857
4858
	$toMinify = array();
4859
	$normal = array();
4860
4861
	uasort(
4862
		$context['css_files'],
4863
		function ($a, $b)
4864
		{
4865
			return $a['options']['order_pos'] < $b['options']['order_pos'] ? -1 : ($a['options']['order_pos'] > $b['options']['order_pos'] ? 1 : 0);
4866
		}
4867
	);
4868
4869
	foreach ($context['css_files'] as $id => $file)
4870
	{
4871
		// Last minute call! allow theme authors to disable single files.
4872
		if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
4873
			continue;
4874
4875
		// Files are minimized unless they explicitly opt out.
4876
		if (!isset($file['options']['minimize']))
4877
			$file['options']['minimize'] = true;
4878
4879
		if (!empty($file['options']['minimize']) && !empty($modSettings['minimize_files']) && !isset($_REQUEST['normalcss']))
4880
		{
4881
			$toMinify[] = $file;
4882
4883
			// Grab a random seed.
4884
			if (!isset($minSeed) && isset($file['options']['seed']))
4885
				$minSeed = $file['options']['seed'];
4886
		}
4887
		else
4888
			$normal[] = array(
4889
				'url' => $file['fileUrl'] . (isset($file['options']['seed']) ? $file['options']['seed'] : ''),
4890
				'attributes' => !empty($file['options']['attributes']) ? $file['options']['attributes'] : array()
4891
			);
4892
	}
4893
4894
	if (!empty($toMinify))
4895
	{
4896
		$result = custMinify($toMinify, 'css');
4897
4898
		$minSuccessful = array_keys($result) === array('smf_minified');
4899
4900
		foreach ($result as $minFile)
4901
			echo '
4902
	<link rel="stylesheet" href="', $minFile['fileUrl'], $minSuccessful && isset($minSeed) ? $minSeed : '', '">';
4903
	}
4904
4905
	// Print the rest after the minified files.
4906
	if (!empty($normal))
4907
		foreach ($normal as $nf)
4908
		{
4909
			echo '
4910
	<link rel="stylesheet" href="', $nf['url'], '"';
4911
4912
			if (!empty($nf['attributes']))
4913
				foreach ($nf['attributes'] as $key => $value)
4914
				{
4915
					if (is_bool($value))
4916
						echo !empty($value) ? ' ' . $key : '';
4917
					else
4918
						echo ' ', $key, '="', $value, '"';
4919
				}
4920
4921
			echo '>';
4922
		}
4923
4924
	if ($db_show_debug === true)
4925
	{
4926
		// Try to keep only what's useful.
4927
		$repl = array($boardurl . '/Themes/' => '', $boardurl . '/' => '');
4928
		foreach ($context['css_files'] as $file)
4929
			$context['debug']['sheets'][] = strtr($file['fileName'], $repl);
4930
	}
4931
4932
	if (!empty($context['css_header']))
4933
	{
4934
		echo '
4935
	<style>';
4936
4937
		foreach ($context['css_header'] as $css)
4938
			echo $css . '
4939
	';
4940
4941
		echo '
4942
	</style>';
4943
	}
4944
}
4945
4946
/**
4947
 * Get an array of previously defined files and adds them to our main minified files.
4948
 * Sets a one day cache to avoid re-creating a file on every request.
4949
 *
4950
 * @param array $data The files to minify.
4951
 * @param string $type either css or js.
4952
 * @return array Info about the minified file, or about the original files if the minify process failed.
4953
 */
4954
function custMinify($data, $type)
4955
{
4956
	global $settings, $txt;
4957
4958
	$types = array('css', 'js');
4959
	$type = !empty($type) && in_array($type, $types) ? $type : false;
4960
	$data = is_array($data) ? $data : array();
0 ignored issues
show
The condition is_array($data) is always true.
Loading history...
4961
4962
	if (empty($type) || empty($data))
4963
		return $data;
4964
4965
	// Different pages include different files, so we use a hash to label the different combinations
4966
	$hash = md5(implode(' ', array_map(
4967
		function($file)
4968
		{
4969
			return $file['filePath'] . '-' . $file['mtime'];
4970
		},
4971
		$data
4972
	)));
4973
4974
	// Is this a deferred or asynchronous JavaScript file?
4975
	$async = $type === 'js';
4976
	$defer = $type === 'js';
4977
	if ($type === 'js')
4978
	{
4979
		foreach ($data as $id => $file)
4980
		{
4981
			// A minified script should only be loaded asynchronously if all its components wanted to be.
4982
			if (empty($file['options']['async']))
4983
				$async = false;
4984
4985
			// A minified script should only be deferred if all its components wanted to be.
4986
			if (empty($file['options']['defer']))
4987
				$defer = false;
4988
		}
4989
	}
4990
4991
	// Did we already do this?
4992
	$minified_file = $settings['theme_dir'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/minified_' . $hash . '.' . $type;
4993
	$already_exists = file_exists($minified_file);
4994
4995
	// Already done?
4996
	if ($already_exists)
4997
	{
4998
		return array('smf_minified' => array(
4999
			'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file),
5000
			'filePath' => $minified_file,
5001
			'fileName' => basename($minified_file),
5002
			'options' => array('async' => !empty($async), 'defer' => !empty($defer)),
5003
		));
5004
	}
5005
	// File has to exist. If it doesn't, try to create it.
5006
	elseif (@fopen($minified_file, 'w') === false || !smf_chmod($minified_file))
5007
	{
5008
		loadLanguage('Errors');
5009
		log_error(sprintf($txt['file_not_created'], $minified_file), 'general');
5010
5011
		// The process failed, so roll back to print each individual file.
5012
		return $data;
5013
	}
5014
5015
	// No namespaces, sorry!
5016
	$classType = 'MatthiasMullie\\Minify\\' . strtoupper($type);
5017
5018
	$minifier = new $classType();
5019
5020
	foreach ($data as $id => $file)
5021
	{
5022
		$toAdd = !empty($file['filePath']) && file_exists($file['filePath']) ? $file['filePath'] : false;
5023
5024
		// The file couldn't be located so it won't be added. Log this error.
5025
		if (empty($toAdd))
5026
		{
5027
			loadLanguage('Errors');
5028
			log_error(sprintf($txt['file_minimize_fail'], !empty($file['fileName']) ? $file['fileName'] : $id), 'general');
5029
			continue;
5030
		}
5031
5032
		// Add this file to the list.
5033
		$minifier->add($toAdd);
5034
	}
5035
5036
	// Create the file.
5037
	$minifier->minify($minified_file);
5038
	unset($minifier);
5039
	clearstatcache();
5040
5041
	// Minify process failed.
5042
	if (!filesize($minified_file))
5043
	{
5044
		loadLanguage('Errors');
5045
		log_error(sprintf($txt['file_not_created'], $minified_file), 'general');
5046
5047
		// The process failed so roll back to print each individual file.
5048
		return $data;
5049
	}
5050
5051
	return array('smf_minified' => array(
5052
		'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file),
5053
		'filePath' => $minified_file,
5054
		'fileName' => basename($minified_file),
5055
		'options' => array('async' => $async, 'defer' => $defer),
5056
	));
5057
}
5058
5059
/**
5060
 * Clears out old minimized CSS and JavaScript files and ensures $modSettings['browser_cache'] is up to date
5061
 */
5062
function deleteAllMinified()
5063
{
5064
	global $smcFunc, $txt, $modSettings;
5065
5066
	$not_deleted = array();
5067
	$most_recent = 0;
5068
5069
	// Kinda sucks that we need to do another query to get all the theme dirs, but c'est la vie.
5070
	$request = $smcFunc['db_query']('', '
5071
		SELECT id_theme AS id, value AS dir
5072
		FROM {db_prefix}themes
5073
		WHERE variable = {string:var}',
5074
		array(
5075
			'var' => 'theme_dir',
5076
		)
5077
	);
5078
	while ($theme = $smcFunc['db_fetch_assoc']($request))
5079
	{
5080
		foreach (array('css', 'js') as $type)
5081
		{
5082
			foreach (glob(rtrim($theme['dir'], '/') . '/' . ($type == 'css' ? 'css' : 'scripts') . '/*.' . $type) as $filename)
5083
			{
5084
				// We want to find the most recent mtime of non-minified files
5085
				if (strpos(pathinfo($filename, PATHINFO_BASENAME), 'minified') === false)
0 ignored issues
show
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

5085
				if (strpos(/** @scrutinizer ignore-type */ pathinfo($filename, PATHINFO_BASENAME), 'minified') === false)
Loading history...
5086
					$most_recent = max($most_recent, (int) @filemtime($filename));
5087
5088
				// Try to delete minified files. Add them to our error list if that fails.
5089
				elseif (!@unlink($filename))
5090
					$not_deleted[] = $filename;
5091
			}
5092
		}
5093
	}
5094
	$smcFunc['db_free_result']($request);
5095
5096
	// This setting tracks the most recent modification time of any of our CSS and JS files
5097
	if ($most_recent != $modSettings['browser_cache'])
5098
		updateSettings(array('browser_cache' => $most_recent));
5099
5100
	// If any of the files could not be deleted, log an error about it.
5101
	if (!empty($not_deleted))
5102
	{
5103
		loadLanguage('Errors');
5104
		log_error(sprintf($txt['unlink_minimized_fail'], implode('<br>', $not_deleted)), 'general');
5105
	}
5106
}
5107
5108
/**
5109
 * Get an attachment's encrypted filename. If $new is true, won't check for file existence.
5110
 *
5111
 * @todo this currently returns the hash if new, and the full filename otherwise.
5112
 * Something messy like that.
5113
 * @todo and of course everything relies on this behavior and work around it. :P.
5114
 * Converters included.
5115
 *
5116
 * @param string $filename The name of the file
5117
 * @param int $attachment_id The ID of the attachment
5118
 * @param string|null $dir Which directory it should be in (null to use current one)
5119
 * @param bool $new Whether this is a new attachment
5120
 * @param string $file_hash The file hash
5121
 * @return string The path to the file
5122
 */
5123
function getAttachmentFilename($filename, $attachment_id, $dir = null, $new = false, $file_hash = '')
5124
{
5125
	global $modSettings, $smcFunc;
5126
5127
	// Just make up a nice hash...
5128
	if ($new)
5129
		return sha1(md5($filename . time()) . mt_rand());
5130
5131
	// Just make sure that attachment id is only a int
5132
	$attachment_id = (int) $attachment_id;
5133
5134
	// Grab the file hash if it wasn't added.
5135
	// Left this for legacy.
5136
	if ($file_hash === '')
5137
	{
5138
		$request = $smcFunc['db_query']('', '
5139
			SELECT file_hash
5140
			FROM {db_prefix}attachments
5141
			WHERE id_attach = {int:id_attach}',
5142
			array(
5143
				'id_attach' => $attachment_id,
5144
			)
5145
		);
5146
5147
		if ($smcFunc['db_num_rows']($request) === 0)
5148
			return false;
5149
5150
		list ($file_hash) = $smcFunc['db_fetch_row']($request);
5151
		$smcFunc['db_free_result']($request);
5152
	}
5153
5154
	// Still no hash? mmm...
5155
	if (empty($file_hash))
5156
		$file_hash = sha1(md5($filename . time()) . mt_rand());
5157
5158
	// Are we using multiple directories?
5159
	if (is_array($modSettings['attachmentUploadDir']))
5160
		$path = $modSettings['attachmentUploadDir'][$dir];
5161
5162
	else
5163
		$path = $modSettings['attachmentUploadDir'];
5164
5165
	return $path . '/' . $attachment_id . '_' . $file_hash . '.dat';
5166
}
5167
5168
/**
5169
 * Convert a single IP to a ranged IP.
5170
 * internal function used to convert a user-readable format to a format suitable for the database.
5171
 *
5172
 * @param string $fullip The full IP
5173
 * @return array An array of IP parts
5174
 */
5175
function ip2range($fullip)
5176
{
5177
	// Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.)
5178
	if ($fullip == 'unknown')
5179
		$fullip = '255.255.255.255';
5180
5181
	$ip_parts = explode('-', $fullip);
5182
	$ip_array = array();
5183
5184
	// if ip 22.12.31.21
5185
	if (count($ip_parts) == 1 && isValidIP($fullip))
5186
	{
5187
		$ip_array['low'] = $fullip;
5188
		$ip_array['high'] = $fullip;
5189
		return $ip_array;
5190
	} // if ip 22.12.* -> 22.12.*-22.12.*
5191
	elseif (count($ip_parts) == 1)
5192
	{
5193
		$ip_parts[0] = $fullip;
5194
		$ip_parts[1] = $fullip;
5195
	}
5196
5197
	// if ip 22.12.31.21-12.21.31.21
5198
	if (count($ip_parts) == 2 && isValidIP($ip_parts[0]) && isValidIP($ip_parts[1]))
5199
	{
5200
		$ip_array['low'] = $ip_parts[0];
5201
		$ip_array['high'] = $ip_parts[1];
5202
		return $ip_array;
5203
	}
5204
	elseif (count($ip_parts) == 2) // if ip 22.22.*-22.22.*
5205
	{
5206
		$valid_low = isValidIP($ip_parts[0]);
5207
		$valid_high = isValidIP($ip_parts[1]);
5208
		$count = 0;
5209
		$mode = (preg_match('/:/', $ip_parts[0]) > 0 ? ':' : '.');
5210
		$max = ($mode == ':' ? 'ffff' : '255');
5211
		$min = 0;
5212
		if (!$valid_low)
5213
		{
5214
			$ip_parts[0] = preg_replace('/\*/', '0', $ip_parts[0]);
5215
			$valid_low = isValidIP($ip_parts[0]);
5216
			while (!$valid_low)
5217
			{
5218
				$ip_parts[0] .= $mode . $min;
5219
				$valid_low = isValidIP($ip_parts[0]);
5220
				$count++;
5221
				if ($count > 9) break;
5222
			}
5223
		}
5224
5225
		$count = 0;
5226
		if (!$valid_high)
5227
		{
5228
			$ip_parts[1] = preg_replace('/\*/', $max, $ip_parts[1]);
5229
			$valid_high = isValidIP($ip_parts[1]);
5230
			while (!$valid_high)
5231
			{
5232
				$ip_parts[1] .= $mode . $max;
5233
				$valid_high = isValidIP($ip_parts[1]);
5234
				$count++;
5235
				if ($count > 9) break;
5236
			}
5237
		}
5238
5239
		if ($valid_high && $valid_low)
5240
		{
5241
			$ip_array['low'] = $ip_parts[0];
5242
			$ip_array['high'] = $ip_parts[1];
5243
		}
5244
	}
5245
5246
	return $ip_array;
5247
}
5248
5249
/**
5250
 * Lookup an IP; try shell_exec first because we can do a timeout on it.
5251
 *
5252
 * @param string $ip The IP to get the hostname from
5253
 * @return string The hostname
5254
 */
5255
function host_from_ip($ip)
5256
{
5257
	global $modSettings;
5258
5259
	if (($host = cache_get_data('hostlookup-' . $ip, 600)) !== null)
5260
		return $host;
5261
	$t = microtime(true);
5262
5263
	$exists = function_exists('shell_exec');
5264
5265
	// Try the Linux host command, perhaps?
5266
	if ($exists && !isset($host) && (strpos(strtolower(PHP_OS), 'win') === false || strpos(strtolower(PHP_OS), 'darwin') !== false) && mt_rand(0, 1) == 1)
5267
	{
5268
		if (!isset($modSettings['host_to_dis']))
5269
			$test = @shell_exec('host -W 1 ' . @escapeshellarg($ip));
5270
		else
5271
			$test = @shell_exec('host ' . @escapeshellarg($ip));
5272
5273
		// Did host say it didn't find anything?
5274
		if (strpos($test, 'not found') !== false)
0 ignored issues
show
It seems like $test can also be of type false and null; 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

5274
		if (strpos(/** @scrutinizer ignore-type */ $test, 'not found') !== false)
Loading history...
5275
			$host = '';
5276
		// Invalid server option?
5277
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
5278
			updateSettings(array('host_to_dis' => 1));
5279
		// Maybe it found something, after all?
5280
		elseif (preg_match('~\s([^\s]+?)\.\s~', $test, $match) == 1)
0 ignored issues
show
It seems like $test can also be of type false and null; 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

5280
		elseif (preg_match('~\s([^\s]+?)\.\s~', /** @scrutinizer ignore-type */ $test, $match) == 1)
Loading history...
5281
			$host = $match[1];
5282
	}
5283
5284
	// This is nslookup; usually only Windows, but possibly some Unix?
5285
	if ($exists && !isset($host) && stripos(PHP_OS, 'win') !== false && strpos(strtolower(PHP_OS), 'darwin') === false && mt_rand(0, 1) == 1)
5286
	{
5287
		$test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
5288
		if (strpos($test, 'Non-existent domain') !== false)
5289
			$host = '';
5290
		elseif (preg_match('~Name:\s+([^\s]+)~', $test, $match) == 1)
5291
			$host = $match[1];
5292
	}
5293
5294
	// This is the last try :/.
5295
	if (!isset($host) || $host === false)
5296
		$host = @gethostbyaddr($ip);
5297
5298
	// It took a long time, so let's cache it!
5299
	if (microtime(true) - $t > 0.5)
5300
		cache_put_data('hostlookup-' . $ip, $host, 600);
5301
5302
	return $host;
5303
}
5304
5305
/**
5306
 * Chops a string into words and prepares them to be inserted into (or searched from) the database.
5307
 *
5308
 * @param string $text The text to split into words
5309
 * @param int $max_chars The maximum number of characters per word
5310
 * @param bool $encrypt Whether to encrypt the results
5311
 * @return array An array of ints or words depending on $encrypt
5312
 */
5313
function text2words($text, $max_chars = 20, $encrypt = false)
5314
{
5315
	global $smcFunc, $context;
5316
5317
	// Upgrader may be working on old DBs...
5318
	if (!isset($context['utf8']))
5319
		$context['utf8'] = false;
5320
5321
	// Step 1: Remove entities/things we don't consider words:
5322
	$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>' => ' ')));
5323
5324
	// Step 2: Entities we left to letters, where applicable, lowercase.
5325
	$words = un_htmlspecialchars($smcFunc['strtolower']($words));
5326
5327
	// Step 3: Ready to split apart and index!
5328
	$words = explode(' ', $words);
5329
5330
	if ($encrypt)
5331
	{
5332
		$possible_chars = array_flip(array_merge(range(46, 57), range(65, 90), range(97, 122)));
5333
		$returned_ints = array();
5334
		foreach ($words as $word)
5335
		{
5336
			if (($word = trim($word, '-_\'')) !== '')
5337
			{
5338
				$encrypted = substr(crypt($word, 'uk'), 2, $max_chars);
5339
				$total = 0;
5340
				for ($i = 0; $i < $max_chars; $i++)
5341
					$total += $possible_chars[ord($encrypted[$i])] * pow(63, $i);
5342
				$returned_ints[] = $max_chars == 4 ? min($total, 16777215) : $total;
5343
			}
5344
		}
5345
		return array_unique($returned_ints);
5346
	}
5347
	else
5348
	{
5349
		// Trim characters before and after and add slashes for database insertion.
5350
		$returned_words = array();
5351
		foreach ($words as $word)
5352
			if (($word = trim($word, '-_\'')) !== '')
5353
				$returned_words[] = $max_chars === null ? $word : $smcFunc['truncate']($word, $max_chars);
5354
5355
		// Filter out all words that occur more than once.
5356
		return array_unique($returned_words);
5357
	}
5358
}
5359
5360
/**
5361
 * Creates an image/text button
5362
 *
5363
 * @deprecated since 2.1
5364
 * @param string $name The name of the button (should be a main_icons class or the name of an image)
5365
 * @param string $alt The alt text
5366
 * @param string $label The $txt string to use as the label
5367
 * @param string $custom Custom text/html to add to the img tag (only when using an actual image)
5368
 * @param boolean $force_use Whether to force use of this when template_create_button is available
5369
 * @return string The HTML to display the button
5370
 */
5371
function create_button($name, $alt, $label = '', $custom = '', $force_use = false)
5372
{
5373
	global $settings, $txt;
5374
5375
	// Does the current loaded theme have this and we are not forcing the usage of this function?
5376
	if (function_exists('template_create_button') && !$force_use)
5377
		return template_create_button($name, $alt, $label = '', $custom = '');
5378
5379
	if (!$settings['use_image_buttons'])
5380
		return $txt[$alt];
5381
	elseif (!empty($settings['use_buttons']))
5382
		return '<span class="main_icons ' . $name . '" alt="' . $txt[$alt] . '"></span>' . ($label != '' ? '&nbsp;<strong>' . $txt[$label] . '</strong>' : '');
5383
	else
5384
		return '<img src="' . $settings['lang_images_url'] . '/' . $name . '" alt="' . $txt[$alt] . '" ' . $custom . '>';
5385
}
5386
5387
/**
5388
 * Sets up all of the top menu buttons
5389
 * Saves them in the cache if it is available and on
5390
 * Places the results in $context
5391
 */
5392
function setupMenuContext()
5393
{
5394
	global $context, $modSettings, $user_info, $txt, $scripturl, $sourcedir, $settings, $smcFunc, $cache_enable;
5395
5396
	// Set up the menu privileges.
5397
	$context['allow_search'] = !empty($modSettings['allow_guestAccess']) ? allowedTo('search_posts') : (!$user_info['is_guest'] && allowedTo('search_posts'));
5398
	$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'));
5399
5400
	$context['allow_memberlist'] = allowedTo('view_mlist');
5401
	$context['allow_calendar'] = allowedTo('calendar_view') && !empty($modSettings['cal_enabled']);
5402
	$context['allow_moderation_center'] = $context['user']['can_mod'];
5403
	$context['allow_pm'] = allowedTo('pm_read');
5404
5405
	$cacheTime = $modSettings['lastActive'] * 60;
5406
5407
	// Initial "can you post an event in the calendar" option - but this might have been set in the calendar already.
5408
	if (!isset($context['allow_calendar_event']))
5409
	{
5410
		$context['allow_calendar_event'] = $context['allow_calendar'] && allowedTo('calendar_post');
5411
5412
		// If you don't allow events not linked to posts and you're not an admin, we have more work to do...
5413
		if ($context['allow_calendar'] && $context['allow_calendar_event'] && empty($modSettings['cal_allow_unlinked']) && !$user_info['is_admin'])
5414
		{
5415
			$boards_can_post = boardsAllowedTo('post_new');
5416
			$context['allow_calendar_event'] &= !empty($boards_can_post);
5417
		}
5418
	}
5419
5420
	// There is some menu stuff we need to do if we're coming at this from a non-guest perspective.
5421
	if (!$context['user']['is_guest'])
5422
	{
5423
		addInlineJavaScript('
5424
	var user_menus = new smc_PopupMenu();
5425
	user_menus.add("profile", "' . $scripturl . '?action=profile;area=popup");
5426
	user_menus.add("alerts", "' . $scripturl . '?action=profile;area=alerts_popup;u=' . $context['user']['id'] . '");', true);
5427
		if ($context['allow_pm'])
5428
			addInlineJavaScript('
5429
	user_menus.add("pm", "' . $scripturl . '?action=pm;sa=popup");', true);
5430
5431
		if (!empty($modSettings['enable_ajax_alerts']))
5432
		{
5433
			require_once($sourcedir . '/Subs-Notify.php');
5434
5435
			$timeout = getNotifyPrefs($context['user']['id'], 'alert_timeout', true);
5436
			$timeout = empty($timeout) ? 10000 : $timeout[$context['user']['id']]['alert_timeout'] * 1000;
5437
5438
			addInlineJavaScript('
5439
	var new_alert_title = "' . $context['forum_name_html_safe'] . '";
5440
	var alert_timeout = ' . $timeout . ';');
5441
			loadJavaScriptFile('alerts.js', array('minimize' => true), 'smf_alerts');
5442
		}
5443
	}
5444
5445
	// All the buttons we can possible want and then some, try pulling the final list of buttons from cache first.
5446
	if (($menu_buttons = cache_get_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $cacheTime)) === null || time() - $cacheTime <= $modSettings['settings_updated'])
5447
	{
5448
		$buttons = array(
5449
			'home' => array(
5450
				'title' => $txt['home'],
5451
				'href' => $scripturl,
5452
				'show' => true,
5453
				'sub_buttons' => array(
5454
				),
5455
				'is_last' => $context['right_to_left'],
5456
			),
5457
			'search' => array(
5458
				'title' => $txt['search'],
5459
				'href' => $scripturl . '?action=search',
5460
				'show' => $context['allow_search'],
5461
				'sub_buttons' => array(
5462
				),
5463
			),
5464
			'admin' => array(
5465
				'title' => $txt['admin'],
5466
				'href' => $scripturl . '?action=admin',
5467
				'show' => $context['allow_admin'],
5468
				'sub_buttons' => array(
5469
					'featuresettings' => array(
5470
						'title' => $txt['modSettings_title'],
5471
						'href' => $scripturl . '?action=admin;area=featuresettings',
5472
						'show' => allowedTo('admin_forum'),
5473
					),
5474
					'packages' => array(
5475
						'title' => $txt['package'],
5476
						'href' => $scripturl . '?action=admin;area=packages',
5477
						'show' => allowedTo('admin_forum'),
5478
					),
5479
					'errorlog' => array(
5480
						'title' => $txt['errorlog'],
5481
						'href' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc',
5482
						'show' => allowedTo('admin_forum') && !empty($modSettings['enableErrorLogging']),
5483
					),
5484
					'permissions' => array(
5485
						'title' => $txt['edit_permissions'],
5486
						'href' => $scripturl . '?action=admin;area=permissions',
5487
						'show' => allowedTo('manage_permissions'),
5488
					),
5489
					'memberapprove' => array(
5490
						'title' => $txt['approve_members_waiting'],
5491
						'href' => $scripturl . '?action=admin;area=viewmembers;sa=browse;type=approve',
5492
						'show' => !empty($context['unapproved_members']),
5493
						'is_last' => true,
5494
					),
5495
				),
5496
			),
5497
			'moderate' => array(
5498
				'title' => $txt['moderate'],
5499
				'href' => $scripturl . '?action=moderate',
5500
				'show' => $context['allow_moderation_center'],
5501
				'sub_buttons' => array(
5502
					'modlog' => array(
5503
						'title' => $txt['modlog_view'],
5504
						'href' => $scripturl . '?action=moderate;area=modlog',
5505
						'show' => !empty($modSettings['modlog_enabled']) && !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
5506
					),
5507
					'poststopics' => array(
5508
						'title' => $txt['mc_unapproved_poststopics'],
5509
						'href' => $scripturl . '?action=moderate;area=postmod;sa=posts',
5510
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
5511
					),
5512
					'attachments' => array(
5513
						'title' => $txt['mc_unapproved_attachments'],
5514
						'href' => $scripturl . '?action=moderate;area=attachmod;sa=attachments',
5515
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
5516
					),
5517
					'reports' => array(
5518
						'title' => $txt['mc_reported_posts'],
5519
						'href' => $scripturl . '?action=moderate;area=reportedposts',
5520
						'show' => !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
5521
					),
5522
					'reported_members' => array(
5523
						'title' => $txt['mc_reported_members'],
5524
						'href' => $scripturl . '?action=moderate;area=reportedmembers',
5525
						'show' => allowedTo('moderate_forum'),
5526
						'is_last' => true,
5527
					)
5528
				),
5529
			),
5530
			'calendar' => array(
5531
				'title' => $txt['calendar'],
5532
				'href' => $scripturl . '?action=calendar',
5533
				'show' => $context['allow_calendar'],
5534
				'sub_buttons' => array(
5535
					'view' => array(
5536
						'title' => $txt['calendar_menu'],
5537
						'href' => $scripturl . '?action=calendar',
5538
						'show' => $context['allow_calendar_event'],
5539
					),
5540
					'post' => array(
5541
						'title' => $txt['calendar_post_event'],
5542
						'href' => $scripturl . '?action=calendar;sa=post',
5543
						'show' => $context['allow_calendar_event'],
5544
						'is_last' => true,
5545
					),
5546
				),
5547
			),
5548
			'mlist' => array(
5549
				'title' => $txt['members_title'],
5550
				'href' => $scripturl . '?action=mlist',
5551
				'show' => $context['allow_memberlist'],
5552
				'sub_buttons' => array(
5553
					'mlist_view' => array(
5554
						'title' => $txt['mlist_menu_view'],
5555
						'href' => $scripturl . '?action=mlist',
5556
						'show' => true,
5557
					),
5558
					'mlist_search' => array(
5559
						'title' => $txt['mlist_search'],
5560
						'href' => $scripturl . '?action=mlist;sa=search',
5561
						'show' => true,
5562
						'is_last' => true,
5563
					),
5564
				),
5565
				'is_last' => !$context['right_to_left'] && empty($settings['login_main_menu']),
5566
			),
5567
			// Theme authors: If you want the login and register buttons to appear in
5568
			// the main forum menu on your theme, set $settings['login_main_menu'] to
5569
			// true in your theme's template_init() function in index.template.php.
5570
			'login' => array(
5571
				'title' => $txt['login'],
5572
				'href' => $scripturl . '?action=login',
5573
				'onclick' => 'return reqOverlayDiv(this.href, ' . JavaScriptEscape($txt['login']) . ', \'login\');',
5574
				'show' => $user_info['is_guest'] && !empty($settings['login_main_menu']),
5575
				'sub_buttons' => array(
5576
				),
5577
				'is_last' => !$context['right_to_left'],
5578
			),
5579
			'logout' => array(
5580
				'title' => $txt['logout'],
5581
				'href' => $scripturl . '?action=logout;' . $context['session_var'] . '=' . $context['session_id'],
5582
				'show' => !$user_info['is_guest'] && !empty($settings['login_main_menu']),
5583
				'sub_buttons' => array(
5584
				),
5585
				'is_last' => !$context['right_to_left'],
5586
			),
5587
			'signup' => array(
5588
				'title' => $txt['register'],
5589
				'href' => $scripturl . '?action=signup',
5590
				'icon' => 'regcenter',
5591
				'show' => $user_info['is_guest'] && $context['can_register'] && !empty($settings['login_main_menu']),
5592
				'sub_buttons' => array(
5593
				),
5594
				'is_last' => !$context['right_to_left'],
5595
			),
5596
		);
5597
5598
		// Allow editing menu buttons easily.
5599
		call_integration_hook('integrate_menu_buttons', array(&$buttons));
5600
5601
		// Now we put the buttons in the context so the theme can use them.
5602
		$menu_buttons = array();
5603
		foreach ($buttons as $act => $button)
5604
			if (!empty($button['show']))
5605
			{
5606
				$button['active_button'] = false;
5607
5608
				// Make sure the last button truly is the last button.
5609
				if (!empty($button['is_last']))
5610
				{
5611
					if (isset($last_button))
5612
						unset($menu_buttons[$last_button]['is_last']);
5613
					$last_button = $act;
5614
				}
5615
5616
				// Go through the sub buttons if there are any.
5617
				if (!empty($button['sub_buttons']))
5618
					foreach ($button['sub_buttons'] as $key => $subbutton)
5619
					{
5620
						if (empty($subbutton['show']))
5621
							unset($button['sub_buttons'][$key]);
5622
5623
						// 2nd level sub buttons next...
5624
						if (!empty($subbutton['sub_buttons']))
5625
						{
5626
							foreach ($subbutton['sub_buttons'] as $key2 => $sub_button2)
5627
							{
5628
								if (empty($sub_button2['show']))
5629
									unset($button['sub_buttons'][$key]['sub_buttons'][$key2]);
5630
							}
5631
						}
5632
					}
5633
5634
				// Does this button have its own icon?
5635
				if (isset($button['icon']) && file_exists($settings['theme_dir'] . '/images/' . $button['icon']))
5636
					$button['icon'] = '<img src="' . $settings['images_url'] . '/' . $button['icon'] . '" alt="">';
5637
				elseif (isset($button['icon']) && file_exists($settings['default_theme_dir'] . '/images/' . $button['icon']))
5638
					$button['icon'] = '<img src="' . $settings['default_images_url'] . '/' . $button['icon'] . '" alt="">';
5639
				elseif (isset($button['icon']))
5640
					$button['icon'] = '<span class="main_icons ' . $button['icon'] . '"></span>';
5641
				else
5642
					$button['icon'] = '<span class="main_icons ' . $act . '"></span>';
5643
5644
				$menu_buttons[$act] = $button;
5645
			}
5646
5647
		if (!empty($cache_enable) && $cache_enable >= 2)
5648
			cache_put_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $menu_buttons, $cacheTime);
5649
	}
5650
5651
	$context['menu_buttons'] = $menu_buttons;
5652
5653
	// Logging out requires the session id in the url.
5654
	if (isset($context['menu_buttons']['logout']))
5655
		$context['menu_buttons']['logout']['href'] = sprintf($context['menu_buttons']['logout']['href'], $context['session_var'], $context['session_id']);
5656
5657
	// Figure out which action we are doing so we can set the active tab.
5658
	// Default to home.
5659
	$current_action = 'home';
5660
5661
	if (isset($context['menu_buttons'][$context['current_action']]))
5662
		$current_action = $context['current_action'];
5663
	elseif ($context['current_action'] == 'search2')
5664
		$current_action = 'search';
5665
	elseif ($context['current_action'] == 'theme')
5666
		$current_action = isset($_REQUEST['sa']) && $_REQUEST['sa'] == 'pick' ? 'profile' : 'admin';
5667
	elseif ($context['current_action'] == 'signup2')
5668
		$current_action = 'signup';
5669
	elseif ($context['current_action'] == 'login2' || ($user_info['is_guest'] && $context['current_action'] == 'reminder'))
5670
		$current_action = 'login';
5671
	elseif ($context['current_action'] == 'groups' && $context['allow_moderation_center'])
5672
		$current_action = 'moderate';
5673
5674
	// There are certain exceptions to the above where we don't want anything on the menu highlighted.
5675
	if ($context['current_action'] == 'profile' && !empty($context['user']['is_owner']))
5676
	{
5677
		$current_action = !empty($_GET['area']) && $_GET['area'] == 'showalerts' ? 'self_alerts' : 'self_profile';
5678
		$context[$current_action] = true;
5679
	}
5680
	elseif ($context['current_action'] == 'pm')
5681
	{
5682
		$current_action = 'self_pm';
5683
		$context['self_pm'] = true;
5684
	}
5685
5686
	$context['total_mod_reports'] = 0;
5687
	$context['total_admin_reports'] = 0;
5688
5689
	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']))
5690
	{
5691
		$context['total_mod_reports'] = $context['open_mod_reports'];
5692
		$context['menu_buttons']['moderate']['sub_buttons']['reports']['amt'] = $context['open_mod_reports'];
5693
	}
5694
5695
	// Show how many errors there are
5696
	if (!empty($context['menu_buttons']['admin']['sub_buttons']['errorlog']))
5697
	{
5698
		// Get an error count, if necessary
5699
		if (!isset($context['num_errors']))
5700
		{
5701
			$query = $smcFunc['db_query']('', '
5702
				SELECT COUNT(*)
5703
				FROM {db_prefix}log_errors',
5704
				array()
5705
			);
5706
5707
			list($context['num_errors']) = $smcFunc['db_fetch_row']($query);
5708
			$smcFunc['db_free_result']($query);
5709
		}
5710
5711
		if (!empty($context['num_errors']))
5712
		{
5713
			$context['total_admin_reports'] += $context['num_errors'];
5714
			$context['menu_buttons']['admin']['sub_buttons']['errorlog']['amt'] = $context['num_errors'];
5715
		}
5716
	}
5717
5718
	// Show number of reported members
5719
	if (!empty($context['open_member_reports']) && !empty($context['menu_buttons']['moderate']['sub_buttons']['reported_members']))
5720
	{
5721
		$context['total_mod_reports'] += $context['open_member_reports'];
5722
		$context['menu_buttons']['moderate']['sub_buttons']['reported_members']['amt'] = $context['open_member_reports'];
5723
	}
5724
5725
	if (!empty($context['unapproved_members']) && !empty($context['menu_buttons']['admin']))
5726
	{
5727
		$context['menu_buttons']['admin']['sub_buttons']['memberapprove']['amt'] = $context['unapproved_members'];
5728
		$context['total_admin_reports'] += $context['unapproved_members'];
5729
	}
5730
5731
	if ($context['total_admin_reports'] > 0 && !empty($context['menu_buttons']['admin']))
5732
	{
5733
		$context['menu_buttons']['admin']['amt'] = $context['total_admin_reports'];
5734
	}
5735
5736
	// Do we have any open reports?
5737
	if ($context['total_mod_reports'] > 0 && !empty($context['menu_buttons']['moderate']))
5738
	{
5739
		$context['menu_buttons']['moderate']['amt'] = $context['total_mod_reports'];
5740
	}
5741
5742
	// Not all actions are simple.
5743
	call_integration_hook('integrate_current_action', array(&$current_action));
5744
5745
	if (isset($context['menu_buttons'][$current_action]))
5746
		$context['menu_buttons'][$current_action]['active_button'] = true;
5747
}
5748
5749
/**
5750
 * Generate a random seed and ensure it's stored in settings.
5751
 */
5752
function smf_seed_generator()
5753
{
5754
	updateSettings(array('rand_seed' => microtime(true)));
5755
}
5756
5757
/**
5758
 * Process functions of an integration hook.
5759
 * calls all functions of the given hook.
5760
 * supports static class method calls.
5761
 *
5762
 * @param string $hook The hook name
5763
 * @param array $parameters An array of parameters this hook implements
5764
 * @return array The results of the functions
5765
 */
5766
function call_integration_hook($hook, $parameters = array())
5767
{
5768
	global $modSettings, $settings, $boarddir, $sourcedir, $db_show_debug;
5769
	global $context, $txt;
5770
5771
	if ($db_show_debug === true)
5772
		$context['debug']['hooks'][] = $hook;
5773
5774
	// Need to have some control.
5775
	if (!isset($context['instances']))
5776
		$context['instances'] = array();
5777
5778
	$results = array();
5779
	if (empty($modSettings[$hook]))
5780
		return $results;
5781
5782
	$functions = explode(',', $modSettings[$hook]);
5783
	// Loop through each function.
5784
	foreach ($functions as $function)
5785
	{
5786
		// Hook has been marked as "disabled". Skip it!
5787
		if (strpos($function, '!') !== false)
5788
			continue;
5789
5790
		$call = call_helper($function, true);
5791
5792
		// Is it valid?
5793
		if (!empty($call))
5794
			$results[$function] = call_user_func_array($call, $parameters);
0 ignored issues
show
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

5794
			$results[$function] = call_user_func_array(/** @scrutinizer ignore-type */ $call, $parameters);
Loading history...
5795
		// This failed, but we want to do so silently.
5796
		elseif (!empty($function) && !empty($context['ignore_hook_errors']))
5797
			return $results;
5798
		// Whatever it was suppose to call, it failed :(
5799
		elseif (!empty($function))
5800
		{
5801
			loadLanguage('Errors');
5802
5803
			// Get a full path to show on error.
5804
			if (strpos($function, '|') !== false)
5805
			{
5806
				list ($file, $string) = explode('|', $function);
5807
				$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'])));
5808
				log_error(sprintf($txt['hook_fail_call_to'], $string, $absPath), 'general');
5809
			}
5810
			// "Assume" the file resides on $boarddir somewhere...
5811
			else
5812
				log_error(sprintf($txt['hook_fail_call_to'], $function, $boarddir), 'general');
5813
		}
5814
	}
5815
5816
	return $results;
5817
}
5818
5819
/**
5820
 * Add a function for integration hook.
5821
 * Does nothing if the function is already added.
5822
 * Cleans up enabled/disabled variants before taking requested action.
5823
 *
5824
 * @param string $hook The complete hook name.
5825
 * @param string $function The function name. Can be a call to a method via Class::method.
5826
 * @param bool $permanent If true, updates the value in settings table.
5827
 * @param string $file The file. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
5828
 * @param bool $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
5829
 */
5830
function add_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
5831
{
5832
	global $smcFunc, $modSettings, $context;
5833
5834
	// Any objects?
5835
	if ($object)
5836
		$function = $function . '#';
5837
5838
	// Any files  to load?
5839
	if (!empty($file) && is_string($file))
5840
		$function = $file . (!empty($function) ? '|' . $function : '');
5841
5842
	// Get the correct string.
5843
	$integration_call = $function;
5844
	$enabled_call = rtrim($function, '!');
5845
	$disabled_call = $enabled_call . '!';
5846
5847
	// Is it going to be permanent?
5848
	if ($permanent)
5849
	{
5850
		$request = $smcFunc['db_query']('', '
5851
			SELECT value
5852
			FROM {db_prefix}settings
5853
			WHERE variable = {string:variable}',
5854
			array(
5855
				'variable' => $hook,
5856
			)
5857
		);
5858
		list ($current_functions) = $smcFunc['db_fetch_row']($request);
5859
		$smcFunc['db_free_result']($request);
5860
5861
		if (!empty($current_functions))
5862
		{
5863
			$current_functions = explode(',', $current_functions);
5864
5865
			// Cleanup enabled/disabled variants before taking action.
5866
			$current_functions = array_diff($current_functions, array($enabled_call, $disabled_call));
5867
5868
			$permanent_functions = array_unique(array_merge($current_functions, array($integration_call)));
5869
		}
5870
		else
5871
			$permanent_functions = array($integration_call);
5872
5873
		updateSettings(array($hook => implode(',', $permanent_functions)));
5874
	}
5875
5876
	// Make current function list usable.
5877
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
5878
5879
	// Cleanup enabled/disabled variants before taking action.
5880
	$functions = array_diff($functions, array($enabled_call, $disabled_call));
5881
5882
	$functions = array_unique(array_merge($functions, array($integration_call)));
5883
	$modSettings[$hook] = implode(',', $functions);
5884
5885
	// It is handy to be able to know which hooks are temporary...
5886
	if ($permanent !== true)
5887
	{
5888
		if (!isset($context['integration_hooks_temporary']))
5889
			$context['integration_hooks_temporary'] = array();
5890
		$context['integration_hooks_temporary'][$hook][$function] = true;
5891
	}
5892
}
5893
5894
/**
5895
 * Remove an integration hook function.
5896
 * Removes the given function from the given hook.
5897
 * Does nothing if the function is not available.
5898
 * Cleans up enabled/disabled variants before taking requested action.
5899
 *
5900
 * @param string $hook The complete hook name.
5901
 * @param string $function The function name. Can be a call to a method via Class::method.
5902
 * @param boolean $permanent Irrelevant for the function itself but need to declare it to match
5903
 * @param string $file The filename. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
5904
 * @param boolean $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
5905
 * @see add_integration_function
5906
 */
5907
function remove_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
5908
{
5909
	global $smcFunc, $modSettings;
5910
5911
	// Any objects?
5912
	if ($object)
5913
		$function = $function . '#';
5914
5915
	// Any files  to load?
5916
	if (!empty($file) && is_string($file))
5917
		$function = $file . '|' . $function;
5918
5919
	// Get the correct string.
5920
	$integration_call = $function;
0 ignored issues
show
The assignment to $integration_call is dead and can be removed.
Loading history...
5921
	$enabled_call = rtrim($function, '!');
5922
	$disabled_call = $enabled_call . '!';
5923
5924
	// Get the permanent functions.
5925
	$request = $smcFunc['db_query']('', '
5926
		SELECT value
5927
		FROM {db_prefix}settings
5928
		WHERE variable = {string:variable}',
5929
		array(
5930
			'variable' => $hook,
5931
		)
5932
	);
5933
	list ($current_functions) = $smcFunc['db_fetch_row']($request);
5934
	$smcFunc['db_free_result']($request);
5935
5936
	if (!empty($current_functions))
5937
	{
5938
		$current_functions = explode(',', $current_functions);
5939
5940
		// Cleanup enabled and disabled variants.
5941
		$current_functions = array_unique(array_diff($current_functions, array($enabled_call, $disabled_call)));
5942
5943
		updateSettings(array($hook => implode(',', $current_functions)));
5944
	}
5945
5946
	// Turn the function list into something usable.
5947
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
5948
5949
	// Cleanup enabled and disabled variants.
5950
	$functions = array_unique(array_diff($functions, array($enabled_call, $disabled_call)));
5951
5952
	$modSettings[$hook] = implode(',', $functions);
5953
}
5954
5955
/**
5956
 * Receives a string and tries to figure it out if its a method or a function.
5957
 * If a method is found, it looks for a "#" which indicates SMF should create a new instance of the given class.
5958
 * Checks the string/array for is_callable() and return false/fatal_lang_error is the given value results in a non callable string/array.
5959
 * Prepare and returns a callable depending on the type of method/function found.
5960
 *
5961
 * @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)
5962
 * @param boolean $return If true, the function will not call the function/method but instead will return the formatted string.
5963
 * @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.
5964
 */
5965
function call_helper($string, $return = false)
5966
{
5967
	global $context, $smcFunc, $txt, $db_show_debug;
5968
5969
	// Really?
5970
	if (empty($string))
5971
		return false;
5972
5973
	// An array? should be a "callable" array IE array(object/class, valid_callable).
5974
	// A closure? should be a callable one.
5975
	if (is_array($string) || $string instanceof Closure)
5976
		return $return ? $string : (is_callable($string) ? call_user_func($string) : false);
5977
5978
	// No full objects, sorry! pass a method or a property instead!
5979
	if (is_object($string))
5980
		return false;
5981
5982
	// Stay vitaminized my friends...
5983
	$string = $smcFunc['htmlspecialchars']($smcFunc['htmltrim']($string));
5984
5985
	// Is there a file to load?
5986
	$string = load_file($string);
5987
5988
	// Loaded file failed
5989
	if (empty($string))
5990
		return false;
5991
5992
	// Found a method.
5993
	if (strpos($string, '::') !== false)
0 ignored issues
show
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

5993
	if (strpos(/** @scrutinizer ignore-type */ $string, '::') !== false)
Loading history...
5994
	{
5995
		list ($class, $method) = explode('::', $string);
0 ignored issues
show
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

5995
		list ($class, $method) = explode('::', /** @scrutinizer ignore-type */ $string);
Loading history...
5996
5997
		// Check if a new object will be created.
5998
		if (strpos($method, '#') !== false)
5999
		{
6000
			// Need to remove the # thing.
6001
			$method = str_replace('#', '', $method);
6002
6003
			// Don't need to create a new instance for every method.
6004
			if (empty($context['instances'][$class]) || !($context['instances'][$class] instanceof $class))
6005
			{
6006
				$context['instances'][$class] = new $class;
6007
6008
				// Add another one to the list.
6009
				if ($db_show_debug === true)
6010
				{
6011
					if (!isset($context['debug']['instances']))
6012
						$context['debug']['instances'] = array();
6013
6014
					$context['debug']['instances'][$class] = $class;
6015
				}
6016
			}
6017
6018
			$func = array($context['instances'][$class], $method);
6019
		}
6020
6021
		// Right then. This is a call to a static method.
6022
		else
6023
			$func = array($class, $method);
6024
	}
6025
6026
	// Nope! just a plain regular function.
6027
	else
6028
		$func = $string;
6029
6030
	// We can't call this helper, but we want to silently ignore this.
6031
	if (!is_callable($func, false, $callable_name) && !empty($context['ignore_hook_errors']))
6032
		return false;
6033
6034
	// Right, we got what we need, time to do some checks.
6035
	elseif (!is_callable($func, false, $callable_name))
6036
	{
6037
		loadLanguage('Errors');
6038
		log_error(sprintf($txt['sub_action_fail'], $callable_name), 'general');
6039
6040
		// Gotta tell everybody.
6041
		return false;
6042
	}
6043
6044
	// Everything went better than expected.
6045
	else
6046
	{
6047
		// What are we gonna do about it?
6048
		if ($return)
6049
			return $func;
6050
6051
		// If this is a plain function, avoid the heat of calling call_user_func().
6052
		else
6053
		{
6054
			if (is_array($func))
6055
				call_user_func($func);
6056
6057
			else
6058
				$func();
6059
		}
6060
	}
6061
}
6062
6063
/**
6064
 * Receives a string and tries to figure it out if it contains info to load a file.
6065
 * Checks for a | (pipe) symbol and tries to load a file with the info given.
6066
 * 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.
6067
 *
6068
 * @param string $string The string containing a valid format.
6069
 * @return string|boolean The given string with the pipe and file info removed. Boolean false if the file couldn't be loaded.
6070
 */
6071
function load_file($string)
6072
{
6073
	global $sourcedir, $txt, $boarddir, $settings, $context;
6074
6075
	if (empty($string))
6076
		return false;
6077
6078
	if (strpos($string, '|') !== false)
6079
	{
6080
		list ($file, $string) = explode('|', $string);
6081
6082
		// Match the wildcards to their regular vars.
6083
		if (empty($settings['theme_dir']))
6084
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
6085
6086
		else
6087
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
6088
6089
		// Load the file if it can be loaded.
6090
		if (file_exists($absPath))
6091
			require_once($absPath);
6092
6093
		// No? try a fallback to $sourcedir
6094
		else
6095
		{
6096
			$absPath = $sourcedir . '/' . $file;
6097
6098
			if (file_exists($absPath))
6099
				require_once($absPath);
6100
6101
			// Sorry, can't do much for you at this point.
6102
			elseif (empty($context['uninstalling']))
6103
			{
6104
				loadLanguage('Errors');
6105
				log_error(sprintf($txt['hook_fail_loading_file'], $absPath), 'general');
6106
6107
				// File couldn't be loaded.
6108
				return false;
6109
			}
6110
		}
6111
	}
6112
6113
	return $string;
6114
}
6115
6116
/**
6117
 * Get the contents of a URL, irrespective of allow_url_fopen.
6118
 *
6119
 * - reads the contents of an http or ftp address and returns the page in a string
6120
 * - will accept up to 3 page redirections (redirectio_level in the function call is private)
6121
 * - if post_data is supplied, the value and length is posted to the given url as form data
6122
 * - URL must be supplied in lowercase
6123
 *
6124
 * @param string $url The URL
6125
 * @param string $post_data The data to post to the given URL
6126
 * @param bool $keep_alive Whether to send keepalive info
6127
 * @param int $redirection_level How many levels of redirection
6128
 * @return string|false The fetched data or false on failure
6129
 */
6130
function fetch_web_data($url, $post_data = '', $keep_alive = false, $redirection_level = 0)
6131
{
6132
	global $webmaster_email, $sourcedir, $txt;
6133
	static $keep_alive_dom = null, $keep_alive_fp = null;
6134
6135
	preg_match('~^(http|ftp)(s)?://([^/:]+)(:(\d+))?(.+)$~', iri_to_url($url), $match);
6136
6137
	// No scheme? No data for you!
6138
	if (empty($match[1]))
6139
		return false;
6140
6141
	// An FTP url. We should try connecting and RETRieving it...
6142
	elseif ($match[1] == 'ftp')
6143
	{
6144
		// Include the file containing the ftp_connection class.
6145
		require_once($sourcedir . '/Class-Package.php');
6146
6147
		// Establish a connection and attempt to enable passive mode.
6148
		$ftp = new ftp_connection(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? 21 : $match[5], 'anonymous', $webmaster_email);
6149
		if ($ftp->error !== false || !$ftp->passive())
0 ignored issues
show
The condition $ftp->error !== false is always true.
Loading history...
6150
			return false;
6151
6152
		// I want that one *points*!
6153
		fwrite($ftp->connection, 'RETR ' . $match[6] . "\r\n");
6154
6155
		// Since passive mode worked (or we would have returned already!) open the connection.
6156
		$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...
6157
		if (!$fp)
6158
			return false;
6159
6160
		// The server should now say something in acknowledgement.
6161
		$ftp->check_response(150);
6162
6163
		$data = '';
6164
		while (!feof($fp))
6165
			$data .= fread($fp, 4096);
6166
		fclose($fp);
6167
6168
		// All done, right?  Good.
6169
		$ftp->check_response(226);
6170
		$ftp->close();
6171
	}
6172
6173
	// This is more likely; a standard HTTP URL.
6174
	elseif (isset($match[1]) && $match[1] == 'http')
6175
	{
6176
		// First try to use fsockopen, because it is fastest.
6177
		if ($keep_alive && $match[3] == $keep_alive_dom)
6178
			$fp = $keep_alive_fp;
6179
		if (empty($fp))
6180
		{
6181
			// Open the socket on the port we want...
6182
			$fp = @fsockopen(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? ($match[2] ? 443 : 80) : $match[5], $err, $err, 5);
6183
		}
6184
		if (!empty($fp))
6185
		{
6186
			if ($keep_alive)
6187
			{
6188
				$keep_alive_dom = $match[3];
6189
				$keep_alive_fp = $fp;
6190
			}
6191
6192
			// I want this, from there, and I'm not going to be bothering you for more (probably.)
6193
			if (empty($post_data))
6194
			{
6195
				fwrite($fp, 'GET ' . ($match[6] !== '/' ? str_replace(' ', '%20', $match[6]) : '') . ' HTTP/1.0' . "\r\n");
6196
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
6197
				fwrite($fp, 'user-agent: '. SMF_USER_AGENT . "\r\n");
6198
				if ($keep_alive)
6199
					fwrite($fp, 'connection: Keep-Alive' . "\r\n\r\n");
6200
				else
6201
					fwrite($fp, 'connection: close' . "\r\n\r\n");
6202
			}
6203
			else
6204
			{
6205
				fwrite($fp, 'POST ' . ($match[6] !== '/' ? $match[6] : '') . ' HTTP/1.0' . "\r\n");
6206
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
6207
				fwrite($fp, 'user-agent: '. SMF_USER_AGENT . "\r\n");
6208
				if ($keep_alive)
6209
					fwrite($fp, 'connection: Keep-Alive' . "\r\n");
6210
				else
6211
					fwrite($fp, 'connection: close' . "\r\n");
6212
				fwrite($fp, 'content-type: application/x-www-form-urlencoded' . "\r\n");
6213
				fwrite($fp, 'content-length: ' . strlen($post_data) . "\r\n\r\n");
6214
				fwrite($fp, $post_data);
6215
			}
6216
6217
			$response = fgets($fp, 768);
6218
6219
			// Redirect in case this location is permanently or temporarily moved.
6220
			if ($redirection_level < 3 && preg_match('~^HTTP/\S+\s+30[127]~i', $response) === 1)
6221
			{
6222
				$header = '';
0 ignored issues
show
The assignment to $header is dead and can be removed.
Loading history...
6223
				$location = '';
6224
				while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
6225
					if (stripos($header, 'location:') !== false)
6226
						$location = trim(substr($header, strpos($header, ':') + 1));
6227
6228
				if (empty($location))
6229
					return false;
6230
				else
6231
				{
6232
					if (!$keep_alive)
6233
						fclose($fp);
6234
					return fetch_web_data($location, $post_data, $keep_alive, $redirection_level + 1);
6235
				}
6236
			}
6237
6238
			// Make sure we get a 200 OK.
6239
			elseif (preg_match('~^HTTP/\S+\s+20[01]~i', $response) === 0)
6240
				return false;
6241
6242
			// Skip the headers...
6243
			while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
6244
			{
6245
				if (preg_match('~content-length:\s*(\d+)~i', $header, $match) != 0)
6246
					$content_length = $match[1];
6247
				elseif (preg_match('~connection:\s*close~i', $header) != 0)
6248
				{
6249
					$keep_alive_dom = null;
6250
					$keep_alive = false;
6251
				}
6252
6253
				continue;
6254
			}
6255
6256
			$data = '';
6257
			if (isset($content_length))
6258
			{
6259
				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...
6260
					$data .= fread($fp, $content_length - strlen($data));
6261
			}
6262
			else
6263
			{
6264
				while (!feof($fp))
6265
					$data .= fread($fp, 4096);
6266
			}
6267
6268
			if (!$keep_alive)
6269
				fclose($fp);
6270
		}
6271
6272
		// If using fsockopen didn't work, try to use cURL if available.
6273
		elseif (function_exists('curl_init'))
6274
		{
6275
			// Include the file containing the curl_fetch_web_data class.
6276
			require_once($sourcedir . '/Class-CurlFetchWeb.php');
6277
6278
			$fetch_data = new curl_fetch_web_data();
6279
			$fetch_data->get_url_data($url, $post_data);
0 ignored issues
show
$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

6279
			$fetch_data->get_url_data($url, /** @scrutinizer ignore-type */ $post_data);
Loading history...
6280
6281
			// no errors and a 200 result, then we have a good dataset, well we at least have data. ;)
6282
			if ($fetch_data->result('code') == 200 && !$fetch_data->result('error'))
6283
				$data = $fetch_data->result('body');
6284
			else
6285
				return false;
6286
		}
6287
6288
		// Neither fsockopen nor curl are available. Well, phooey.
6289
		else
6290
			return false;
6291
	}
6292
	else
6293
	{
6294
		// Umm, this shouldn't happen?
6295
		loadLanguage('Errors');
6296
		trigger_error($txt['fetch_web_data_bad_url'], E_USER_NOTICE);
6297
		$data = false;
6298
	}
6299
6300
	return $data;
6301
}
6302
6303
/**
6304
 * Attempts to determine the MIME type of some data or a file.
6305
 *
6306
 * @param string $data The data to check, or the path or URL of a file to check.
6307
 * @param string $is_path If true, $data is a path or URL to a file.
6308
 * @return string|bool A MIME type, or false if we cannot determine it.
6309
 */
6310
function get_mime_type($data, $is_path = false)
6311
{
6312
	global $cachedir;
6313
6314
	$finfo_loaded = extension_loaded('fileinfo');
6315
	$exif_loaded = extension_loaded('exif') && function_exists('image_type_to_mime_type');
6316
6317
	// Oh well. We tried.
6318
	if (!$finfo_loaded && !$exif_loaded)
6319
		return false;
6320
6321
	// Start with the 'empty' MIME type.
6322
	$mime_type = 'application/x-empty';
6323
6324
	if ($finfo_loaded)
6325
	{
6326
		// Just some nice, simple data to analyze.
6327
		if (empty($is_path))
6328
			$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
6329
6330
		// A file, or maybe a URL?
6331
		else
6332
		{
6333
			// Local file.
6334
			if (file_exists($data))
6335
				$mime_type = mime_content_type($data);
6336
6337
			// URL.
6338
			elseif ($data = fetch_web_data($data))
6339
				$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
6340
		}
6341
	}
6342
	// Workaround using Exif requires a local file.
6343
	else
6344
	{
6345
		// If $data is a URL to fetch, do so.
6346
		if (!empty($is_path) && !file_exists($data) && url_exists($data))
6347
		{
6348
			$data = fetch_web_data($data);
6349
			$is_path = false;
6350
		}
6351
6352
		// If we don't have a local file, create one and use it.
6353
		if (empty($is_path))
6354
		{
6355
			$temp_file = tempnam($cachedir, md5($data));
0 ignored issues
show
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

6355
			$temp_file = tempnam($cachedir, md5(/** @scrutinizer ignore-type */ $data));
Loading history...
6356
			file_put_contents($temp_file, $data);
6357
			$is_path = true;
0 ignored issues
show
The assignment to $is_path is dead and can be removed.
Loading history...
6358
			$data = $temp_file;
6359
		}
6360
6361
		$imagetype = @exif_imagetype($data);
0 ignored issues
show
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

6361
		$imagetype = @exif_imagetype(/** @scrutinizer ignore-type */ $data);
Loading history...
6362
6363
		if (isset($temp_file))
6364
			unlink($temp_file);
6365
6366
		// Unfortunately, this workaround only works for image files.
6367
		if ($imagetype !== false)
6368
			$mime_type = image_type_to_mime_type($imagetype);
6369
	}
6370
6371
	return $mime_type;
6372
}
6373
6374
/**
6375
 * Checks whether a file or data has the expected MIME type.
6376
 *
6377
 * @param string $data The data to check, or the path or URL of a file to check.
6378
 * @param string $type_pattern A regex pattern to match the acceptable MIME types.
6379
 * @param string $is_path If true, $data is a path or URL to a file.
6380
 * @return int 1 if the detected MIME type matches the pattern, 0 if it doesn't, or 2 if we can't check.
6381
 */
6382
function check_mime_type($data, $type_pattern, $is_path = false)
6383
{
6384
	// Get the MIME type.
6385
	$mime_type = get_mime_type($data, $is_path);
0 ignored issues
show
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

6385
	$mime_type = get_mime_type($data, /** @scrutinizer ignore-type */ $is_path);
Loading history...
6386
6387
	// Couldn't determine it.
6388
	if ($mime_type === false)
6389
		return 2;
6390
6391
	// Check whether the MIME type matches expectations.
6392
	return (int) @preg_match('~' . $type_pattern . '~', $mime_type);
6393
}
6394
6395
/**
6396
 * Prepares an array of "likes" info for the topic specified by $topic
6397
 *
6398
 * @param integer $topic The topic ID to fetch the info from.
6399
 * @return array An array of IDs of messages in the specified topic that the current user likes
6400
 */
6401
function prepareLikesContext($topic)
6402
{
6403
	global $user_info, $smcFunc;
6404
6405
	// Make sure we have something to work with.
6406
	if (empty($topic))
6407
		return array();
6408
6409
	// We already know the number of likes per message, we just want to know whether the current user liked it or not.
6410
	$user = $user_info['id'];
6411
	$cache_key = 'likes_topic_' . $topic . '_' . $user;
6412
	$ttl = 180;
6413
6414
	if (($temp = cache_get_data($cache_key, $ttl)) === null)
6415
	{
6416
		$temp = array();
6417
		$request = $smcFunc['db_query']('', '
6418
			SELECT content_id
6419
			FROM {db_prefix}user_likes AS l
6420
				INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg)
6421
			WHERE l.id_member = {int:current_user}
6422
				AND l.content_type = {literal:msg}
6423
				AND m.id_topic = {int:topic}',
6424
			array(
6425
				'current_user' => $user,
6426
				'topic' => $topic,
6427
			)
6428
		);
6429
		while ($row = $smcFunc['db_fetch_assoc']($request))
6430
			$temp[] = (int) $row['content_id'];
6431
6432
		cache_put_data($cache_key, $temp, $ttl);
6433
	}
6434
6435
	return $temp;
6436
}
6437
6438
/**
6439
 * Microsoft uses their own character set Code Page 1252 (CP1252), which is a
6440
 * superset of ISO 8859-1, defining several characters between DEC 128 and 159
6441
 * that are not normally displayable.  This converts the popular ones that
6442
 * appear from a cut and paste from windows.
6443
 *
6444
 * @param string $string The string
6445
 * @return string The sanitized string
6446
 */
6447
function sanitizeMSCutPaste($string)
6448
{
6449
	global $context;
6450
6451
	if (empty($string))
6452
		return $string;
6453
6454
	// UTF-8 occurences of MS special characters
6455
	$findchars_utf8 = array(
6456
		"\xe2\x80\x9a",	// single low-9 quotation mark
6457
		"\xe2\x80\x9e",	// double low-9 quotation mark
6458
		"\xe2\x80\xa6",	// horizontal ellipsis
6459
		"\xe2\x80\x98",	// left single curly quote
6460
		"\xe2\x80\x99",	// right single curly quote
6461
		"\xe2\x80\x9c",	// left double curly quote
6462
		"\xe2\x80\x9d",	// right double curly quote
6463
	);
6464
6465
	// windows 1252 / iso equivalents
6466
	$findchars_iso = array(
6467
		chr(130),
6468
		chr(132),
6469
		chr(133),
6470
		chr(145),
6471
		chr(146),
6472
		chr(147),
6473
		chr(148),
6474
	);
6475
6476
	// safe replacements
6477
	$replacechars = array(
6478
		',',	// &sbquo;
6479
		',,',	// &bdquo;
6480
		'...',	// &hellip;
6481
		"'",	// &lsquo;
6482
		"'",	// &rsquo;
6483
		'"',	// &ldquo;
6484
		'"',	// &rdquo;
6485
	);
6486
6487
	if ($context['utf8'])
6488
		$string = str_replace($findchars_utf8, $replacechars, $string);
6489
	else
6490
		$string = str_replace($findchars_iso, $replacechars, $string);
6491
6492
	return $string;
6493
}
6494
6495
/**
6496
 * Decode numeric html entities to their ascii or UTF8 equivalent character.
6497
 *
6498
 * Callback function for preg_replace_callback in subs-members
6499
 * Uses capture group 2 in the supplied array
6500
 * Does basic scan to ensure characters are inside a valid range
6501
 *
6502
 * @param array $matches An array of matches (relevant info should be the 3rd item)
6503
 * @return string A fixed string
6504
 */
6505
function replaceEntities__callback($matches)
6506
{
6507
	global $context;
6508
6509
	if (!isset($matches[2]))
6510
		return '';
6511
6512
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
0 ignored issues
show
$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

6512
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
6513
6514
	// remove left to right / right to left overrides
6515
	if ($num === 0x202D || $num === 0x202E)
6516
		return '';
6517
6518
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
6519
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
6520
		return '&#' . $num . ';';
6521
6522
	if (empty($context['utf8']))
6523
	{
6524
		// no control characters
6525
		if ($num < 0x20)
6526
			return '';
6527
		// text is text
6528
		elseif ($num < 0x80)
6529
			return chr($num);
0 ignored issues
show
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

6529
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
6530
		// all others get html-ised
6531
		else
6532
			return '&#' . $matches[2] . ';';
0 ignored issues
show
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

6532
			return '&#' . /** @scrutinizer ignore-type */ $matches[2] . ';';
Loading history...
6533
	}
6534
	else
6535
	{
6536
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
6537
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
6538
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
6539
			return '';
6540
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
6541
		elseif ($num < 0x80)
6542
			return chr($num);
6543
		// <0x800 (2048)
6544
		elseif ($num < 0x800)
6545
			return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
6546
		// < 0x10000 (65536)
6547
		elseif ($num < 0x10000)
6548
			return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6549
		// <= 0x10FFFF (1114111)
6550
		else
6551
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6552
	}
6553
}
6554
6555
/**
6556
 * Converts html entities to utf8 equivalents
6557
 *
6558
 * Callback function for preg_replace_callback
6559
 * Uses capture group 1 in the supplied array
6560
 * Does basic checks to keep characters inside a viewable range.
6561
 *
6562
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
6563
 * @return string The fixed string
6564
 */
6565
function fixchar__callback($matches)
6566
{
6567
	if (!isset($matches[1]))
6568
		return '';
6569
6570
	$num = $matches[1][0] === 'x' ? hexdec(substr($matches[1], 1)) : (int) $matches[1];
0 ignored issues
show
$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

6570
	$num = $matches[1][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[1], 1)) : (int) $matches[1];
Loading history...
6571
6572
	// <0x20 are control characters, > 0x10FFFF is past the end of the utf8 character set
6573
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text), 0x202D-E are left to right overrides
6574
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202D || $num === 0x202E)
6575
		return '';
6576
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
6577
	elseif ($num < 0x80)
6578
		return chr($num);
0 ignored issues
show
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

6578
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
6579
	// <0x800 (2048)
6580
	elseif ($num < 0x800)
6581
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
6582
	// < 0x10000 (65536)
6583
	elseif ($num < 0x10000)
6584
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6585
	// <= 0x10FFFF (1114111)
6586
	else
6587
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6588
}
6589
6590
/**
6591
 * Strips out invalid html entities, replaces others with html style &#123; codes
6592
 *
6593
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
6594
 * strpos, strlen, substr etc
6595
 *
6596
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
6597
 * @return string The fixed string
6598
 */
6599
function entity_fix__callback($matches)
6600
{
6601
	if (!isset($matches[2]))
6602
		return '';
6603
6604
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
0 ignored issues
show
$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

6604
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
6605
6606
	// we don't allow control characters, characters out of range, byte markers, etc
6607
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
6608
		return '';
6609
	else
6610
		return '&#' . $num . ';';
6611
}
6612
6613
/**
6614
 * Return a Gravatar URL based on
6615
 * - the supplied email address,
6616
 * - the global maximum rating,
6617
 * - the global default fallback,
6618
 * - maximum sizes as set in the admin panel.
6619
 *
6620
 * It is SSL aware, and caches most of the parameters.
6621
 *
6622
 * @param string $email_address The user's email address
6623
 * @return string The gravatar URL
6624
 */
6625
function get_gravatar_url($email_address)
6626
{
6627
	global $modSettings, $smcFunc;
6628
	static $url_params = null;
6629
6630
	if ($url_params === null)
6631
	{
6632
		$ratings = array('G', 'PG', 'R', 'X');
6633
		$defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank');
6634
		$url_params = array();
6635
		if (!empty($modSettings['gravatarMaxRating']) && in_array($modSettings['gravatarMaxRating'], $ratings))
6636
			$url_params[] = 'rating=' . $modSettings['gravatarMaxRating'];
6637
		if (!empty($modSettings['gravatarDefault']) && in_array($modSettings['gravatarDefault'], $defaults))
6638
			$url_params[] = 'default=' . $modSettings['gravatarDefault'];
6639
		if (!empty($modSettings['avatar_max_width_external']))
6640
			$size_string = (int) $modSettings['avatar_max_width_external'];
6641
		if (!empty($modSettings['avatar_max_height_external']) && !empty($size_string))
6642
			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...
6643
				$size_string = $modSettings['avatar_max_height_external'];
6644
6645
		if (!empty($size_string))
6646
			$url_params[] = 's=' . $size_string;
6647
	}
6648
	$http_method = !empty($modSettings['force_ssl']) ? 'https://secure' : 'http://www';
6649
6650
	return $http_method . '.gravatar.com/avatar/' . md5($smcFunc['strtolower']($email_address)) . '?' . implode('&', $url_params);
6651
}
6652
6653
/**
6654
 * Get a list of time zones.
6655
 *
6656
 * @param string $when The date/time for which to calculate the time zone values.
6657
 *		May be a Unix timestamp or any string that strtotime() can understand.
6658
 *		Defaults to 'now'.
6659
 * @return array An array of time zone identifiers and label text.
6660
 */
6661
function smf_list_timezones($when = 'now')
6662
{
6663
	global $modSettings, $tztxt, $txt, $context, $cur_profile, $sourcedir;
6664
	static $timezones_when = array();
6665
6666
	require_once($sourcedir . '/Subs-Timezones.php');
6667
6668
	// Parseable datetime string?
6669
	if (is_int($timestamp = strtotime($when)))
0 ignored issues
show
The condition is_int($timestamp = strtotime($when)) is always true.
Loading history...
6670
		$when = $timestamp;
6671
6672
	// A Unix timestamp?
6673
	elseif (is_numeric($when))
6674
		$when = intval($when);
6675
6676
	// Invalid value? Just get current Unix timestamp.
6677
	else
6678
		$when = time();
6679
6680
	// No point doing this over if we already did it once
6681
	if (isset($timezones_when[$when]))
6682
		return $timezones_when[$when];
6683
6684
	// We'll need these too
6685
	$date_when = date_create('@' . $when);
6686
	$later = strtotime('@' . $when . ' + 1 year');
6687
6688
	// Load up any custom time zone descriptions we might have
6689
	loadLanguage('Timezones');
6690
6691
	$tzid_metazones = get_tzid_metazones($later);
6692
6693
	// Should we put time zones from certain countries at the top of the list?
6694
	$priority_countries = !empty($modSettings['timezone_priority_countries']) ? explode(',', $modSettings['timezone_priority_countries']) : array();
6695
6696
	$priority_tzids = array();
6697
	foreach ($priority_countries as $country)
6698
	{
6699
		$country_tzids = get_sorted_tzids_for_country($country);
6700
6701
		if (!empty($country_tzids))
6702
			$priority_tzids = array_merge($priority_tzids, $country_tzids);
6703
	}
6704
6705
	// Antarctic research stations should be listed last, unless you're running a penguin forum
6706
	$low_priority_tzids = !in_array('AQ', $priority_countries) ? timezone_identifiers_list(DateTimeZone::ANTARCTICA) : array();
0 ignored issues
show
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...
6707
6708
	$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
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...
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

6708
	$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...
6709
6710
	// Process them in order of importance.
6711
	$tzids = array_merge($priority_tzids, $normal_priority_tzids, $low_priority_tzids);
6712
6713
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
6714
	$dst_types = array();
6715
	$labels = array();
6716
	$offsets = array();
6717
	foreach ($tzids as $tzid)
6718
	{
6719
		// We don't want UTC right now
6720
		if ($tzid == 'UTC')
6721
			continue;
6722
6723
		$tz = @timezone_open($tzid);
6724
6725
		if ($tz == null)
6726
			continue;
6727
6728
		// First, get the set of transition rules for this tzid
6729
		$tzinfo = timezone_transitions_get($tz, $when, $later);
6730
6731
		// Use the entire set of transition rules as the array *key* so we can avoid duplicates
6732
		$tzkey = serialize($tzinfo);
6733
6734
		// ...But make sure to include all explicitly defined meta-zones.
6735
		if (isset($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid]))
6736
			$tzkey = serialize(array_merge($tzinfo, array('metazone' => $tzid_metazones[$tzid])));
6737
6738
		// Don't overwrite our preferred tzids
6739
		if (empty($zones[$tzkey]['tzid']))
6740
		{
6741
			$zones[$tzkey]['tzid'] = $tzid;
6742
			$zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
6743
6744
			foreach ($tzinfo as $transition) {
6745
				$zones[$tzkey]['abbrs'][] = $transition['abbr'];
6746
			}
6747
6748
			if (isset($tzid_metazones[$tzid]))
6749
				$zones[$tzkey]['metazone'] = $tzid_metazones[$tzid];
6750
			else
6751
			{
6752
				$tzgeo = timezone_location_get($tz);
6753
				$country_tzids = get_sorted_tzids_for_country($tzgeo['country_code']);
6754
6755
				if (count($country_tzids) === 1)
6756
					$zones[$tzkey]['metazone'] = $txt['iso3166'][$tzgeo['country_code']];
6757
			}
6758
		}
6759
6760
		// A time zone from a prioritized country?
6761
		if (in_array($tzid, $priority_tzids))
6762
			$priority_zones[$tzkey] = true;
6763
6764
		// Keep track of the location for this tzid.
6765
		if (!empty($txt[$tzid]))
6766
			$zones[$tzkey]['locations'][] = $txt[$tzid];
6767
		else
6768
		{
6769
			$tzid_parts = explode('/', $tzid);
6770
			$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
6771
		}
6772
6773
		// Keep track of the current offset for this tzid.
6774
		$offsets[$tzkey] = $tzinfo[0]['offset'];
6775
6776
		// Keep track of the Standard Time offset for this tzid.
6777
		foreach ($tzinfo as $transition)
6778
		{
6779
			if (!$transition['isdst'])
6780
			{
6781
				$std_offsets[$tzkey] = $transition['offset'];
6782
				break;
6783
			}
6784
		}
6785
		if (!isset($std_offsets[$tzkey]))
6786
			$std_offsets[$tzkey] = $tzinfo[0]['offset'];
6787
6788
		// Figure out the "meta-zone" info for the label
6789
		if (empty($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid]))
6790
		{
6791
			$zones[$tzkey]['metazone'] = $tzid_metazones[$tzid];
6792
			$zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
6793
		}
6794
		$dst_types[$tzkey] = count($tzinfo) > 1 ? 'c' : ($tzinfo[0]['isdst'] ? 't' : 'f');
6795
		$labels[$tzkey] = !empty($zones[$tzkey]['metazone']) && !empty($tztxt[$zones[$tzkey]['metazone']]) ? $tztxt[$zones[$tzkey]['metazone']] : '';
6796
6797
		// Remember this for later
6798
		if (isset($cur_profile['timezone']) && $cur_profile['timezone'] == $tzid)
6799
			$member_tzkey = $tzkey;
6800
		if (isset($context['event']['tz']) && $context['event']['tz'] == $tzid)
6801
			$event_tzkey = $tzkey;
6802
		if ($modSettings['default_timezone'] == $tzid)
6803
			$default_tzkey = $tzkey;
6804
	}
6805
6806
	// Sort by current offset, then standard offset, then DST type, then label.
6807
	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
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

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

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

6807
	array_multisort($offsets, /** @scrutinizer ignore-type */ SORT_DESC, SORT_NUMERIC, $std_offsets, SORT_DESC, SORT_NUMERIC, $dst_types, SORT_ASC, $labels, SORT_ASC, $zones);
Loading history...
Comprehensibility Best Practice introduced by
The variable $std_offsets does not seem to be defined for all execution paths leading up to this point.
Loading history...
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...
6808
6809
	// Build the final array of formatted values
6810
	$priority_timezones = array();
6811
	$timezones = array();
6812
	foreach ($zones as $tzkey => $tzvalue)
6813
	{
6814
		date_timezone_set($date_when, timezone_open($tzvalue['tzid']));
6815
6816
		// Use the human friendly time zone name, if there is one.
6817
		$desc = '';
6818
		if (!empty($tzvalue['metazone']))
6819
		{
6820
			if (!empty($tztxt[$tzvalue['metazone']]))
6821
				$metazone = $tztxt[$tzvalue['metazone']];
6822
			else
6823
				$metazone = sprintf($tztxt['generic_timezone'], $tzvalue['metazone'], '%1$s');
6824
6825
			switch ($tzvalue['dst_type'])
6826
			{
6827
				case 0:
6828
					$desc = sprintf($metazone, $tztxt['daylight_saving_time_false']);
6829
					break;
6830
6831
				case 1:
6832
					$desc = sprintf($metazone, '');
6833
					break;
6834
6835
				case 2:
6836
					$desc = sprintf($metazone, $tztxt['daylight_saving_time_true']);
6837
					break;
6838
			}
6839
		}
6840
		// Otherwise, use the list of locations (max 5, so things don't get silly)
6841
		else
6842
			$desc = implode(', ', array_slice(array_unique($tzvalue['locations']), 0, 5)) . (count($tzvalue['locations']) > 5 ? ', ' . $txt['etc'] : '');
6843
6844
		// We don't want abbreviations like '+03' or '-11'.
6845
		$abbrs = array_filter(
6846
			$tzvalue['abbrs'],
6847
			function ($abbr)
6848
			{
6849
				return !strspn($abbr, '+-');
6850
			}
6851
		);
6852
		$abbrs = count($abbrs) == count($tzvalue['abbrs']) ? array_unique($abbrs) : array();
6853
6854
		// Show the UTC offset and abbreviation(s).
6855
		$desc = '[UTC' . date_format($date_when, 'P') . '] - ' . str_replace('  ', ' ', $desc) . (!empty($abbrs) ? ' (' . implode('/', $abbrs) . ')' : '');
6856
6857
		if (isset($priority_zones[$tzkey]))
6858
			$priority_timezones[$tzvalue['tzid']] = $desc;
6859
		else
6860
			$timezones[$tzvalue['tzid']] = $desc;
6861
6862
		// Automatically fix orphaned time zones.
6863
		if (isset($member_tzkey) && $member_tzkey == $tzkey)
6864
			$cur_profile['timezone'] = $tzvalue['tzid'];
6865
		if (isset($event_tzkey) && $event_tzkey == $tzkey)
6866
			$context['event']['tz'] = $tzvalue['tzid'];
6867
		if (isset($default_tzkey) && $default_tzkey == $tzkey && $modSettings['default_timezone'] != $tzvalue['tzid'])
6868
			updateSettings(array('default_timezone' => $tzvalue['tzid']));
6869
	}
6870
6871
	if (!empty($priority_timezones))
6872
		$priority_timezones[] = '-----';
6873
6874
	$timezones = array_merge(
6875
		$priority_timezones,
6876
		array('UTC' => 'UTC' . (!empty($tztxt['UTC']) ? ' - ' . $tztxt['UTC'] : ''), '-----'),
6877
		$timezones
6878
	);
6879
6880
	$timezones_when[$when] = $timezones;
6881
6882
	return $timezones_when[$when];
6883
}
6884
6885
/**
6886
 * Gets a member's selected time zone identifier
6887
 *
6888
 * @param int $id_member The member id to look up. If not provided, the current user's id will be used.
6889
 * @return string The time zone identifier string for the user's time zone.
6890
 */
6891
function getUserTimezone($id_member = null)
6892
{
6893
	global $smcFunc, $user_info, $modSettings, $user_settings;
6894
	static $member_cache = array();
6895
6896
	if (is_null($id_member))
6897
		$id_member = empty($user_info['id']) ? 0 : (int) $user_info['id'];
6898
	else
6899
		$id_member = (int) $id_member;
6900
6901
	// Did we already look this up?
6902
	if (isset($member_cache[$id_member]))
6903
		return $member_cache[$id_member];
6904
6905
	// Check if we already have this in $user_settings.
6906
	if (isset($user_settings['id_member']) && $user_settings['id_member'] == $id_member && !empty($user_settings['timezone']))
6907
	{
6908
		$member_cache[$id_member] = $user_settings['timezone'];
6909
		return $user_settings['timezone'];
6910
	}
6911
6912
	if (!empty($id_member))
6913
	{
6914
		// Look it up in the database.
6915
		$request = $smcFunc['db_query']('', '
6916
			SELECT timezone
6917
			FROM {db_prefix}members
6918
			WHERE id_member = {int:id_member}',
6919
			array(
6920
				'id_member' => $id_member,
6921
			)
6922
		);
6923
		list($timezone) = $smcFunc['db_fetch_row']($request);
6924
		$smcFunc['db_free_result']($request);
6925
	}
6926
6927
	// If it is invalid, fall back to the default.
6928
	if (empty($timezone) || !in_array($timezone, timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)))
0 ignored issues
show
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

6928
	if (empty($timezone) || !in_array($timezone, /** @scrutinizer ignore-type */ timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)))
Loading history...
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...
6929
		$timezone = isset($modSettings['default_timezone']) ? $modSettings['default_timezone'] : date_default_timezone_get();
6930
6931
	// Save for later.
6932
	$member_cache[$id_member] = $timezone;
6933
6934
	return $timezone;
6935
}
6936
6937
/**
6938
 * Converts an IP address into binary
6939
 *
6940
 * @param string $ip_address An IP address in IPv4, IPv6 or decimal notation
6941
 * @return string|false The IP address in binary or false
6942
 */
6943
function inet_ptod($ip_address)
6944
{
6945
	if (!isValidIP($ip_address))
6946
		return $ip_address;
6947
6948
	$bin = inet_pton($ip_address);
6949
	return $bin;
6950
}
6951
6952
/**
6953
 * Converts a binary version of an IP address into a readable format
6954
 *
6955
 * @param string $bin An IP address in IPv4, IPv6 (Either string (postgresql) or binary (other databases))
6956
 * @return string|false The IP address in presentation format or false on error
6957
 */
6958
function inet_dtop($bin)
6959
{
6960
	global $db_type;
6961
6962
	if (empty($bin))
6963
		return '';
6964
	elseif ($db_type == 'postgresql')
6965
		return $bin;
6966
	// Already a String?
6967
	elseif (isValidIP($bin))
6968
		return $bin;
6969
	return inet_ntop($bin);
6970
}
6971
6972
/**
6973
 * Safe serialize() and unserialize() replacements
6974
 *
6975
 * @license Public Domain
6976
 *
6977
 * @author anthon (dot) pang (at) gmail (dot) com
6978
 */
6979
6980
/**
6981
 * Safe serialize() replacement. Recursive
6982
 * - output a strict subset of PHP's native serialized representation
6983
 * - does not serialize objects
6984
 *
6985
 * @param mixed $value
6986
 * @return string
6987
 */
6988
function _safe_serialize($value)
6989
{
6990
	if (is_null($value))
6991
		return 'N;';
6992
6993
	if (is_bool($value))
6994
		return 'b:' . (int) $value . ';';
6995
6996
	if (is_int($value))
6997
		return 'i:' . $value . ';';
6998
6999
	if (is_float($value))
7000
		return 'd:' . str_replace(',', '.', $value) . ';';
7001
7002
	if (is_string($value))
7003
		return 's:' . strlen($value) . ':"' . $value . '";';
7004
7005
	if (is_array($value))
7006
	{
7007
		// Check for nested objects or resources.
7008
		$contains_invalid = false;
7009
		array_walk_recursive(
7010
			$value,
7011
			function($v) use (&$contains_invalid)
7012
			{
7013
				if (is_object($v) || is_resource($v))
7014
					$contains_invalid = true;
7015
			}
7016
		);
7017
		if ($contains_invalid)
7018
			return false;
7019
7020
		$out = '';
7021
		foreach ($value as $k => $v)
7022
			$out .= _safe_serialize($k) . _safe_serialize($v);
7023
7024
		return 'a:' . count($value) . ':{' . $out . '}';
7025
	}
7026
7027
	// safe_serialize cannot serialize resources or objects.
7028
	return false;
7029
}
7030
7031
/**
7032
 * Wrapper for _safe_serialize() that handles exceptions and multibyte encoding issues.
7033
 *
7034
 * @param mixed $value
7035
 * @return string
7036
 */
7037
function safe_serialize($value)
7038
{
7039
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
7040
	if (function_exists('mb_internal_encoding') &&
7041
		(((int) ini_get('mbstring.func_overload')) & 2))
7042
	{
7043
		$mbIntEnc = mb_internal_encoding();
7044
		mb_internal_encoding('ASCII');
7045
	}
7046
7047
	$out = _safe_serialize($value);
7048
7049
	if (isset($mbIntEnc))
7050
		mb_internal_encoding($mbIntEnc);
7051
7052
	return $out;
7053
}
7054
7055
/**
7056
 * Safe unserialize() replacement
7057
 * - accepts a strict subset of PHP's native serialized representation
7058
 * - does not unserialize objects
7059
 *
7060
 * @param string $str
7061
 * @return mixed
7062
 * @throw Exception if $str is malformed or contains unsupported types (e.g., resources, objects)
7063
 */
7064
function _safe_unserialize($str)
7065
{
7066
	// Input  is not a string.
7067
	if (empty($str) || !is_string($str))
7068
		return false;
7069
7070
	// The substrings 'O' and 'C' are used to serialize objects.
7071
	// If they are not present, then there are none in the serialized data.
7072
	if (strpos($str, 'O:') === false && strpos($str, 'C:') === false)
7073
		return unserialize($str, ['allowed_classes' => false]);
7074
7075
	$stack = array();
7076
	$expected = array();
7077
7078
	/*
7079
	 * states:
7080
	 *   0 - initial state, expecting a single value or array
7081
	 *   1 - terminal state
7082
	 *   2 - in array, expecting end of array or a key
7083
	 *   3 - in array, expecting value or another array
7084
	 */
7085
	$state = 0;
7086
	while ($state != 1)
7087
	{
7088
		$type = isset($str[0]) ? $str[0] : '';
7089
		if ($type == '}')
7090
			$str = substr($str, 1);
7091
7092
		elseif ($type == 'N' && $str[1] == ';')
7093
		{
7094
			$value = null;
7095
			$str = substr($str, 2);
7096
		}
7097
		elseif ($type == 'b' && preg_match('/^b:([01]);/', $str, $matches))
7098
		{
7099
			$value = $matches[1] == '1' ? true : false;
7100
			$str = substr($str, 4);
7101
		}
7102
		elseif ($type == 'i' && preg_match('/^i:(-?[0-9]+);(.*)/s', $str, $matches))
7103
		{
7104
			$value = (int) $matches[1];
7105
			$str = $matches[2];
7106
		}
7107
		elseif ($type == 'd' && preg_match('/^d:(-?[0-9]+\.?[0-9]*(E[+-][0-9]+)?);(.*)/s', $str, $matches))
7108
		{
7109
			$value = (float) $matches[1];
7110
			$str = $matches[3];
7111
		}
7112
		elseif ($type == 's' && preg_match('/^s:([0-9]+):"(.*)/s', $str, $matches) && substr($matches[2], (int) $matches[1], 2) == '";')
7113
		{
7114
			$value = substr($matches[2], 0, (int) $matches[1]);
7115
			$str = substr($matches[2], (int) $matches[1] + 2);
7116
		}
7117
		elseif ($type == 'a' && preg_match('/^a:([0-9]+):{(.*)/s', $str, $matches))
7118
		{
7119
			$expectedLength = (int) $matches[1];
7120
			$str = $matches[2];
7121
		}
7122
7123
		// Object or unknown/malformed type.
7124
		else
7125
			return false;
7126
7127
		switch ($state)
7128
		{
7129
			case 3: // In array, expecting value or another array.
7130
				if ($type == 'a')
7131
				{
7132
					$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...
7133
					$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...
7134
					$list = &$list[$key];
7135
					$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...
7136
					$state = 2;
7137
					break;
7138
				}
7139
				if ($type != '}')
7140
				{
7141
					$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...
7142
					$state = 2;
7143
					break;
7144
				}
7145
7146
				// Missing array value.
7147
				return false;
7148
7149
			case 2: // in array, expecting end of array or a key
7150
				if ($type == '}')
7151
				{
7152
					// Array size is less than expected.
7153
					if (count($list) < end($expected))
7154
						return false;
7155
7156
					unset($list);
7157
					$list = &$stack[count($stack) - 1];
7158
					array_pop($stack);
7159
7160
					// Go to terminal state if we're at the end of the root array.
7161
					array_pop($expected);
7162
7163
					if (count($expected) == 0)
7164
						$state = 1;
7165
7166
					break;
7167
				}
7168
7169
				if ($type == 'i' || $type == 's')
7170
				{
7171
					// Array size exceeds expected length.
7172
					if (count($list) >= end($expected))
7173
						return false;
7174
7175
					$key = $value;
7176
					$state = 3;
7177
					break;
7178
				}
7179
7180
				// Illegal array index type.
7181
				return false;
7182
7183
			// Expecting array or value.
7184
			case 0:
7185
				if ($type == 'a')
7186
				{
7187
					$data = array();
7188
					$list = &$data;
7189
					$expected[] = $expectedLength;
7190
					$state = 2;
7191
					break;
7192
				}
7193
7194
				if ($type != '}')
7195
				{
7196
					$data = $value;
7197
					$state = 1;
7198
					break;
7199
				}
7200
7201
				// Not in array.
7202
				return false;
7203
		}
7204
	}
7205
7206
	// Trailing data in input.
7207
	if (!empty($str))
7208
		return false;
7209
7210
	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...
7211
}
7212
7213
/**
7214
 * Wrapper for _safe_unserialize() that handles exceptions and multibyte encoding issue
7215
 *
7216
 * @param string $str
7217
 * @return mixed
7218
 */
7219
function safe_unserialize($str)
7220
{
7221
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
7222
	if (function_exists('mb_internal_encoding') &&
7223
		(((int) ini_get('mbstring.func_overload')) & 0x02))
7224
	{
7225
		$mbIntEnc = mb_internal_encoding();
7226
		mb_internal_encoding('ASCII');
7227
	}
7228
7229
	$out = _safe_unserialize($str);
7230
7231
	if (isset($mbIntEnc))
7232
		mb_internal_encoding($mbIntEnc);
7233
7234
	return $out;
7235
}
7236
7237
/**
7238
 * Tries different modes to make file/dirs writable. Wrapper function for chmod()
7239
 *
7240
 * @param string $file The file/dir full path.
7241
 * @param int $value Not needed, added for legacy reasons.
7242
 * @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.
7243
 */
7244
function smf_chmod($file, $value = 0)
7245
{
7246
	// No file? no checks!
7247
	if (empty($file))
7248
		return false;
7249
7250
	// Already writable?
7251
	if (is_writable($file))
7252
		return true;
7253
7254
	// Do we have a file or a dir?
7255
	$isDir = is_dir($file);
7256
	$isWritable = false;
7257
7258
	// Set different modes.
7259
	$chmodValues = $isDir ? array(0750, 0755, 0775, 0777) : array(0644, 0664, 0666);
7260
7261
	foreach ($chmodValues as $val)
7262
	{
7263
		// If it's writable, break out of the loop.
7264
		if (is_writable($file))
7265
		{
7266
			$isWritable = true;
7267
			break;
7268
		}
7269
7270
		else
7271
			@chmod($file, $val);
7272
	}
7273
7274
	return $isWritable;
7275
}
7276
7277
/**
7278
 * Wrapper function for json_decode() with error handling.
7279
 *
7280
 * @param string $json The string to decode.
7281
 * @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.
7282
 * @param bool $logIt To specify if the error will be logged if theres any.
7283
 * @return array Either an empty array or the decoded data as an array.
7284
 */
7285
function smf_json_decode($json, $returnAsArray = false, $logIt = true)
7286
{
7287
	global $txt;
7288
7289
	// Come on...
7290
	if (empty($json) || !is_string($json))
7291
		return array();
7292
7293
	$returnArray = @json_decode($json, $returnAsArray);
7294
7295
	// PHP 5.3 so no json_last_error_msg()
7296
	switch (json_last_error())
7297
	{
7298
		case JSON_ERROR_NONE:
7299
			$jsonError = false;
7300
			break;
7301
		case JSON_ERROR_DEPTH:
7302
			$jsonError = 'JSON_ERROR_DEPTH';
7303
			break;
7304
		case JSON_ERROR_STATE_MISMATCH:
7305
			$jsonError = 'JSON_ERROR_STATE_MISMATCH';
7306
			break;
7307
		case JSON_ERROR_CTRL_CHAR:
7308
			$jsonError = 'JSON_ERROR_CTRL_CHAR';
7309
			break;
7310
		case JSON_ERROR_SYNTAX:
7311
			$jsonError = 'JSON_ERROR_SYNTAX';
7312
			break;
7313
		case JSON_ERROR_UTF8:
7314
			$jsonError = 'JSON_ERROR_UTF8';
7315
			break;
7316
		default:
7317
			$jsonError = 'unknown';
7318
			break;
7319
	}
7320
7321
	// Something went wrong!
7322
	if (!empty($jsonError) && $logIt)
7323
	{
7324
		// Being a wrapper means we lost our smf_error_handler() privileges :(
7325
		$jsonDebug = debug_backtrace();
7326
		$jsonDebug = $jsonDebug[0];
7327
		loadLanguage('Errors');
7328
7329
		if (!empty($jsonDebug))
7330
			log_error($txt['json_' . $jsonError], 'critical', $jsonDebug['file'], $jsonDebug['line']);
7331
7332
		else
7333
			log_error($txt['json_' . $jsonError], 'critical');
7334
7335
		// Everyone expects an array.
7336
		return array();
7337
	}
7338
7339
	return $returnArray;
7340
}
7341
7342
/**
7343
 * Check the given String if he is a valid IPv4 or IPv6
7344
 * return true or false
7345
 *
7346
 * @param string $IPString
7347
 *
7348
 * @return bool
7349
 */
7350
function isValidIP($IPString)
7351
{
7352
	return filter_var($IPString, FILTER_VALIDATE_IP) !== false;
7353
}
7354
7355
/**
7356
 * Outputs a response.
7357
 * It assumes the data is already a string.
7358
 *
7359
 * @param string $data The data to print
7360
 * @param string $type The content type. Defaults to Json.
7361
 * @return void
7362
 */
7363
function smf_serverResponse($data = '', $type = 'content-type: application/json')
7364
{
7365
	global $db_show_debug, $modSettings;
7366
7367
	// Defensive programming anyone?
7368
	if (empty($data))
7369
		return false;
7370
7371
	// Don't need extra stuff...
7372
	$db_show_debug = false;
7373
7374
	// Kill anything else.
7375
	ob_end_clean();
7376
7377
	if (!empty($modSettings['enableCompressedOutput']))
7378
		@ob_start('ob_gzhandler');
7379
	else
7380
		ob_start();
7381
7382
	// Set the header.
7383
	header($type);
7384
7385
	// Echo!
7386
	echo $data;
7387
7388
	// Done.
7389
	obExit(false);
7390
}
7391
7392
/**
7393
 * Creates an optimized regex to match all known top level domains.
7394
 *
7395
 * The optimized regex is stored in $modSettings['tld_regex'].
7396
 *
7397
 * To update the stored version of the regex to use the latest list of valid
7398
 * TLDs from iana.org, set the $update parameter to true. Updating can take some
7399
 * time, based on network connectivity, so it should normally only be done by
7400
 * calling this function from a background or scheduled task.
7401
 *
7402
 * If $update is not true, but the regex is missing or invalid, the regex will
7403
 * be regenerated from a hard-coded list of TLDs. This regenerated regex will be
7404
 * overwritten on the next scheduled update.
7405
 *
7406
 * @param bool $update If true, fetch and process the latest official list of TLDs from iana.org.
7407
 */
7408
function set_tld_regex($update = false)
7409
{
7410
	global $sourcedir, $smcFunc, $modSettings;
7411
	static $done = false;
7412
7413
	// If we don't need to do anything, don't
7414
	if (!$update && $done)
7415
		return;
7416
7417
	// Should we get a new copy of the official list of TLDs?
7418
	if ($update)
7419
	{
7420
		$tlds = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt');
7421
		$tlds_md5 = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt.md5');
7422
7423
		/**
7424
		 * If the Internet Assigned Numbers Authority can't be reached, the Internet is GONE!
7425
		 * We're probably running on a server hidden in a bunker deep underground to protect
7426
		 * it from marauding bandits roaming on the surface. We don't want to waste precious
7427
		 * electricity on pointlessly repeating background tasks, so we'll wait until the next
7428
		 * regularly scheduled update to see if civilization has been restored.
7429
		 */
7430
		if ($tlds === false || $tlds_md5 === false)
7431
			$postapocalypticNightmare = true;
7432
7433
		// Make sure nothing went horribly wrong along the way.
7434
		if (md5($tlds) != substr($tlds_md5, 0, 32))
0 ignored issues
show
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

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

7434
		if (md5($tlds) != substr(/** @scrutinizer ignore-type */ $tlds_md5, 0, 32))
Loading history...
7435
			$tlds = array();
7436
	}
7437
	// If we aren't updating and the regex is valid, we're done
7438
	elseif (!empty($modSettings['tld_regex']) && @preg_match('~' . $modSettings['tld_regex'] . '~', '') !== false)
7439
	{
7440
		$done = true;
7441
		return;
7442
	}
7443
7444
	// If we successfully got an update, process the list into an array
7445
	if (!empty($tlds))
7446
	{
7447
		// Clean $tlds and convert it to an array
7448
		$tlds = array_filter(
7449
			explode("\n", strtolower($tlds)),
7450
			function($line)
7451
			{
7452
				$line = trim($line);
7453
				if (empty($line) || strlen($line) != strspn($line, 'abcdefghijklmnopqrstuvwxyz0123456789-'))
7454
					return false;
7455
				else
7456
					return true;
7457
			}
7458
		);
7459
7460
		// Convert Punycode to Unicode
7461
		if (!function_exists('idn_to_utf8'))
7462
			require_once($sourcedir . '/Subs-Compat.php');
7463
7464
		foreach ($tlds as &$tld)
7465
			$tld = idn_to_utf8($tld, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
7466
	}
7467
	// Otherwise, use the 2012 list of gTLDs and ccTLDs for now and schedule a background update
7468
	else
7469
	{
7470
		$tlds = array('com', 'net', 'org', 'edu', 'gov', 'mil', 'aero', 'asia', 'biz',
7471
			'cat', 'coop', 'info', 'int', 'jobs', 'mobi', 'museum', 'name', 'post',
7472
			'pro', 'tel', 'travel', 'xxx', 'ac', 'ad', 'ae', 'af', 'ag', 'ai', 'al',
7473
			'am', 'ao', 'aq', 'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd',
7474
			'be', 'bf', 'bg', 'bh', 'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv',
7475
			'bw', 'by', 'bz', 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm',
7476
			'cn', 'co', 'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do',
7477
			'dz', 'ec', 'ee', 'eg', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo',
7478
			'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp',
7479
			'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu',
7480
			'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm', 'jo',
7481
			'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la',
7482
			'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md',
7483
			'me', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp', 'mq', 'mr', 'ms', 'mt',
7484
			'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf', 'ng', 'ni', 'nl',
7485
			'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl',
7486
			'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw',
7487
			'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn',
7488
			'so', 'sr', 'ss', 'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg',
7489
			'th', 'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua',
7490
			'ug', 'uk', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf',
7491
			'ws', 'ye', 'yt', 'za', 'zm', 'zw',
7492
		);
7493
7494
		// Schedule a background update, unless civilization has collapsed and/or we are having connectivity issues.
7495
		if (empty($postapocalypticNightmare))
7496
		{
7497
			$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
7498
				array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
7499
				array('$sourcedir/tasks/UpdateTldRegex.php', 'Update_TLD_Regex', '', 0), array()
7500
			);
7501
		}
7502
	}
7503
7504
	// Tack on some "special use domain names" that aren't in DNS but may possibly resolve.
7505
	// See https://www.iana.org/assignments/special-use-domain-names/ for more info.
7506
	$tlds = array_merge($tlds, array('local', 'onion', 'test'));
7507
7508
	// Get an optimized regex to match all the TLDs
7509
	$tld_regex = build_regex($tlds);
7510
7511
	// Remember the new regex in $modSettings
7512
	updateSettings(array('tld_regex' => $tld_regex));
7513
7514
	// Redundant repetition is redundant
7515
	$done = true;
7516
}
7517
7518
/**
7519
 * Creates optimized regular expressions from an array of strings.
7520
 *
7521
 * An optimized regex built using this function will be much faster than a
7522
 * simple regex built using `implode('|', $strings)` --- anywhere from several
7523
 * times to several orders of magnitude faster.
7524
 *
7525
 * However, the time required to build the optimized regex is approximately
7526
 * equal to the time it takes to execute the simple regex. Therefore, it is only
7527
 * worth calling this function if the resulting regex will be used more than
7528
 * once.
7529
 *
7530
 * Because PHP places an upper limit on the allowed length of a regex, very
7531
 * large arrays of $strings may not fit in a single regex. Normally, the excess
7532
 * strings will simply be dropped. However, if the $returnArray parameter is set
7533
 * to true, this function will build as many regexes as necessary to accommodate
7534
 * everything in $strings and return them in an array. You will need to iterate
7535
 * through all elements of the returned array in order to test all possible
7536
 * matches.
7537
 *
7538
 * @param array $strings An array of strings to make a regex for.
7539
 * @param string $delim An optional delimiter character to pass to preg_quote().
7540
 * @param bool $returnArray If true, returns an array of regexes.
7541
 * @return string|array One or more regular expressions to match any of the input strings.
7542
 */
7543
function build_regex($strings, $delim = null, $returnArray = false)
7544
{
7545
	global $smcFunc;
7546
	static $regexes = array();
7547
7548
	// If it's not an array, there's not much to do. ;)
7549
	if (!is_array($strings))
0 ignored issues
show
The condition is_array($strings) is always true.
Loading history...
7550
		return preg_quote(@strval($strings), $delim);
7551
7552
	$regex_key = md5(json_encode(array($strings, $delim, $returnArray)));
7553
7554
	if (isset($regexes[$regex_key]))
7555
		return $regexes[$regex_key];
7556
7557
	// The mb_* functions are faster than the $smcFunc ones, but may not be available
7558
	if (function_exists('mb_internal_encoding') && function_exists('mb_detect_encoding') && function_exists('mb_strlen') && function_exists('mb_substr'))
7559
	{
7560
		if (($string_encoding = mb_detect_encoding(implode(' ', $strings))) !== false)
7561
		{
7562
			$current_encoding = mb_internal_encoding();
7563
			mb_internal_encoding($string_encoding);
7564
		}
7565
7566
		$strlen = 'mb_strlen';
7567
		$substr = 'mb_substr';
7568
	}
7569
	else
7570
	{
7571
		$strlen = $smcFunc['strlen'];
7572
		$substr = $smcFunc['substr'];
7573
	}
7574
7575
	// This recursive function creates the index array from the strings
7576
	$add_string_to_index = function($string, $index) use (&$strlen, &$substr, &$add_string_to_index)
7577
	{
7578
		static $depth = 0;
7579
		$depth++;
7580
7581
		$first = (string) @$substr($string, 0, 1);
7582
7583
		// No first character? That's no good.
7584
		if ($first === '')
7585
		{
7586
			// A nested array? Really? Ugh. Fine.
7587
			if (is_array($string) && $depth < 20)
7588
			{
7589
				foreach ($string as $str)
7590
					$index = $add_string_to_index($str, $index);
7591
			}
7592
7593
			$depth--;
7594
			return $index;
7595
		}
7596
7597
		if (empty($index[$first]))
7598
			$index[$first] = array();
7599
7600
		if ($strlen($string) > 1)
7601
		{
7602
			// Sanity check on recursion
7603
			if ($depth > 99)
7604
				$index[$first][$substr($string, 1)] = '';
7605
7606
			else
7607
				$index[$first] = $add_string_to_index($substr($string, 1), $index[$first]);
7608
		}
7609
		else
7610
			$index[$first][''] = '';
7611
7612
		$depth--;
7613
		return $index;
7614
	};
7615
7616
	// This recursive function turns the index array into a regular expression
7617
	$index_to_regex = function(&$index, $delim) use (&$strlen, &$index_to_regex)
7618
	{
7619
		static $depth = 0;
7620
		$depth++;
7621
7622
		// Absolute max length for a regex is 32768, but we might need wiggle room
7623
		$max_length = 30000;
7624
7625
		$regex = array();
7626
		$length = 0;
7627
7628
		foreach ($index as $key => $value)
7629
		{
7630
			$key_regex = preg_quote($key, $delim);
7631
			$new_key = $key;
7632
7633
			if (empty($value))
7634
				$sub_regex = '';
7635
			else
7636
			{
7637
				$sub_regex = $index_to_regex($value, $delim);
7638
7639
				if (count(array_keys($value)) == 1)
7640
				{
7641
					$new_key_array = explode('(?' . '>', $sub_regex);
7642
					$new_key .= $new_key_array[0];
7643
				}
7644
				else
7645
					$sub_regex = '(?' . '>' . $sub_regex . ')';
7646
			}
7647
7648
			if ($depth > 1)
7649
				$regex[$new_key] = $key_regex . $sub_regex;
7650
			else
7651
			{
7652
				if (($length += strlen($key_regex . $sub_regex) + 1) < $max_length || empty($regex))
7653
				{
7654
					$regex[$new_key] = $key_regex . $sub_regex;
7655
					unset($index[$key]);
7656
				}
7657
				else
7658
					break;
7659
			}
7660
		}
7661
7662
		// Sort by key length and then alphabetically
7663
		uksort(
7664
			$regex,
7665
			function($k1, $k2) use (&$strlen)
7666
			{
7667
				$l1 = $strlen($k1);
7668
				$l2 = $strlen($k2);
7669
7670
				if ($l1 == $l2)
7671
					return strcmp($k1, $k2) > 0 ? 1 : -1;
7672
				else
7673
					return $l1 > $l2 ? -1 : 1;
7674
			}
7675
		);
7676
7677
		$depth--;
7678
		return implode('|', $regex);
7679
	};
7680
7681
	// Now that the functions are defined, let's do this thing
7682
	$index = array();
7683
	$regex = '';
7684
7685
	foreach ($strings as $string)
7686
		$index = $add_string_to_index($string, $index);
7687
7688
	if ($returnArray === true)
7689
	{
7690
		$regex = array();
7691
		while (!empty($index))
7692
			$regex[] = '(?' . '>' . $index_to_regex($index, $delim) . ')';
7693
	}
7694
	else
7695
		$regex = '(?' . '>' . $index_to_regex($index, $delim) . ')';
7696
7697
	// Restore PHP's internal character encoding to whatever it was originally
7698
	if (!empty($current_encoding))
7699
		mb_internal_encoding($current_encoding);
7700
7701
	$regexes[$regex_key] = $regex;
7702
	return $regex;
7703
}
7704
7705
/**
7706
 * Check if the passed url has an SSL certificate.
7707
 *
7708
 * Returns true if a cert was found & false if not.
7709
 *
7710
 * @param string $url to check, in $boardurl format (no trailing slash).
7711
 */
7712
function ssl_cert_found($url)
7713
{
7714
	// This check won't work without OpenSSL
7715
	if (!extension_loaded('openssl'))
7716
		return true;
7717
7718
	// First, strip the subfolder from the passed url, if any
7719
	$parsedurl = parse_iri($url);
7720
	$url = 'ssl://' . $parsedurl['host'] . ':443';
7721
7722
	// Next, check the ssl stream context for certificate info
7723
	if (version_compare(PHP_VERSION, '5.6.0', '<'))
7724
		$ssloptions = array("capture_peer_cert" => true);
7725
	else
7726
		$ssloptions = array("capture_peer_cert" => true, "verify_peer" => true, "allow_self_signed" => true);
7727
7728
	$result = false;
7729
	$context = stream_context_create(array("ssl" => $ssloptions));
7730
	$stream = @stream_socket_client($url, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
7731
	if ($stream !== false)
7732
	{
7733
		$params = stream_context_get_params($stream);
7734
		$result = isset($params["options"]["ssl"]["peer_certificate"]) ? true : false;
7735
	}
7736
	return $result;
7737
}
7738
7739
/**
7740
 * Check if the passed url has a redirect to https:// by querying headers.
7741
 *
7742
 * Returns true if a redirect was found & false if not.
7743
 * Note that when force_ssl = 2, SMF issues its own redirect...  So if this
7744
 * returns true, it may be caused by SMF, not necessarily an .htaccess redirect.
7745
 *
7746
 * @param string $url to check, in $boardurl format (no trailing slash).
7747
 */
7748
function https_redirect_active($url)
7749
{
7750
	// Ask for the headers for the passed url, but via http...
7751
	// Need to add the trailing slash, or it puts it there & thinks there's a redirect when there isn't...
7752
	$url = str_ireplace('https://', 'http://', $url) . '/';
0 ignored issues
show
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

7752
	$url = /** @scrutinizer ignore-type */ str_ireplace('https://', 'http://', $url) . '/';
Loading history...
7753
	$headers = @get_headers($url);
7754
	if ($headers === false)
7755
		return false;
7756
7757
	// Now to see if it came back https...
7758
	// First check for a redirect status code in first row (301, 302, 307)
7759
	if (strstr($headers[0], '301') === false && strstr($headers[0], '302') === false && strstr($headers[0], '307') === false)
7760
		return false;
7761
7762
	// Search for the location entry to confirm https
7763
	$result = false;
7764
	foreach ($headers as $header)
7765
	{
7766
		if (stristr($header, 'Location: https://') !== false)
7767
		{
7768
			$result = true;
7769
			break;
7770
		}
7771
	}
7772
	return $result;
7773
}
7774
7775
/**
7776
 * Build query_wanna_see_board and query_see_board for a userid
7777
 *
7778
 * Returns array with keys query_wanna_see_board and query_see_board
7779
 *
7780
 * @param int $userid of the user
7781
 */
7782
function build_query_board($userid)
7783
{
7784
	global $user_info, $modSettings, $smcFunc, $db_prefix;
7785
7786
	$query_part = array();
7787
7788
	// If we come from cron, we can't have a $user_info.
7789
	if (isset($user_info['id']) && $user_info['id'] == $userid && SMF != 'BACKGROUND')
7790
	{
7791
		$groups = $user_info['groups'];
7792
		$can_see_all_boards = $user_info['is_admin'] || $user_info['can_manage_boards'];
7793
		$ignoreboards = !empty($user_info['ignoreboards']) ? $user_info['ignoreboards'] : null;
7794
	}
7795
	else
7796
	{
7797
		$request = $smcFunc['db_query']('', '
7798
			SELECT mem.ignore_boards, mem.id_group, mem.additional_groups, mem.id_post_group
7799
			FROM {db_prefix}members AS mem
7800
			WHERE mem.id_member = {int:id_member}
7801
			LIMIT 1',
7802
			array(
7803
				'id_member' => $userid,
7804
			)
7805
		);
7806
7807
		$row = $smcFunc['db_fetch_assoc']($request);
7808
7809
		if (empty($row['additional_groups']))
7810
			$groups = array($row['id_group'], $row['id_post_group']);
7811
		else
7812
			$groups = array_merge(
7813
				array($row['id_group'], $row['id_post_group']),
7814
				explode(',', $row['additional_groups'])
7815
			);
7816
7817
		// Because history has proven that it is possible for groups to go bad - clean up in case.
7818
		foreach ($groups as $k => $v)
7819
			$groups[$k] = (int) $v;
7820
7821
		$can_see_all_boards = in_array(1, $groups) || (!empty($modSettings['board_manager_groups']) && count(array_intersect($groups, explode(',', $modSettings['board_manager_groups']))) > 0);
7822
7823
		$ignoreboards = !empty($row['ignore_boards']) && !empty($modSettings['allow_ignore_boards']) ? explode(',', $row['ignore_boards']) : array();
7824
	}
7825
7826
	// Just build this here, it makes it easier to change/use - administrators can see all boards.
7827
	if ($can_see_all_boards)
7828
		$query_part['query_see_board'] = '1=1';
7829
	// Otherwise just the groups in $user_info['groups'].
7830
	else
7831
	{
7832
		$query_part['query_see_board'] = '
7833
			EXISTS (
7834
				SELECT bpv.id_board
7835
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
7836
				WHERE bpv.id_group IN ('. implode(',', $groups) .')
7837
					AND bpv.deny = 0
7838
					AND bpv.id_board = b.id_board
7839
			)';
7840
7841
		if (!empty($modSettings['deny_boards_access']))
7842
			$query_part['query_see_board'] .= '
7843
			AND NOT EXISTS (
7844
				SELECT bpv.id_board
7845
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
7846
				WHERE bpv.id_group IN ( '. implode(',', $groups) .')
7847
					AND bpv.deny = 1
7848
					AND bpv.id_board = b.id_board
7849
			)';
7850
	}
7851
7852
	$query_part['query_see_message_board'] = str_replace('b.', 'm.', $query_part['query_see_board']);
7853
	$query_part['query_see_topic_board'] = str_replace('b.', 't.', $query_part['query_see_board']);
7854
7855
	// Build the list of boards they WANT to see.
7856
	// This will take the place of query_see_boards in certain spots, so it better include the boards they can see also
7857
7858
	// If they aren't ignoring any boards then they want to see all the boards they can see
7859
	if (empty($ignoreboards))
7860
	{
7861
		$query_part['query_wanna_see_board'] = $query_part['query_see_board'];
7862
		$query_part['query_wanna_see_message_board'] = $query_part['query_see_message_board'];
7863
		$query_part['query_wanna_see_topic_board'] = $query_part['query_see_topic_board'];
7864
	}
7865
	// Ok I guess they don't want to see all the boards
7866
	else
7867
	{
7868
		$query_part['query_wanna_see_board'] = '(' . $query_part['query_see_board'] . ' AND b.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7869
		$query_part['query_wanna_see_message_board'] = '(' . $query_part['query_see_message_board'] . ' AND m.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7870
		$query_part['query_wanna_see_topic_board'] = '(' . $query_part['query_see_topic_board'] . ' AND t.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7871
	}
7872
7873
	return $query_part;
7874
}
7875
7876
/**
7877
 * Check if the connection is using https.
7878
 *
7879
 * @return boolean true if connection used https
7880
 */
7881
function httpsOn()
7882
{
7883
	$secure = false;
7884
7885
	if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on')
7886
		$secure = true;
7887
	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
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...
7888
		$secure = true;
7889
7890
	return $secure;
7891
}
7892
7893
/**
7894
 * A wrapper for `parse_url($url)` that can handle URLs with international
7895
 * characters (a.k.a. IRIs)
7896
 *
7897
 * @param string $iri The IRI to parse.
7898
 * @param int $component Optional parameter to pass to parse_url().
7899
 * @return mixed Same as parse_url(), but with unmangled Unicode.
7900
 */
7901
function parse_iri($iri, $component = -1)
7902
{
7903
	$iri = preg_replace_callback(
7904
		'~[^\x00-\x7F\pZ\pC]|%~u',
7905
		function($matches)
7906
		{
7907
			return rawurlencode($matches[0]);
7908
		},
7909
		$iri
7910
	);
7911
7912
	$parsed = parse_url($iri, $component);
7913
7914
	if (is_array($parsed))
0 ignored issues
show
The condition is_array($parsed) is always false.
Loading history...
7915
	{
7916
		foreach ($parsed as &$part)
7917
			$part = rawurldecode($part);
7918
	}
7919
	elseif (is_string($parsed))
0 ignored issues
show
The condition is_string($parsed) is always true.
Loading history...
7920
		$parsed = rawurldecode($parsed);
7921
7922
	return $parsed;
7923
}
7924
7925
/**
7926
 * A wrapper for `filter_var($url, FILTER_VALIDATE_URL)` that can handle URLs
7927
 * with international characters (a.k.a. IRIs)
7928
 *
7929
 * @param string $iri The IRI to test.
7930
 * @param int $flags Optional flags to pass to filter_var()
7931
 * @return string|bool Either the original IRI, or false if the IRI was invalid.
7932
 */
7933
function validate_iri($iri, $flags = 0)
7934
{
7935
	$url = iri_to_url($iri);
7936
7937
	// PHP 5 doesn't recognize IPv6 addresses in the URL host.
7938
	if (version_compare(phpversion(), '7.0.0', '<'))
7939
	{
7940
		$host = parse_url((strpos($url, '//') === 0 ? 'http:' : '') . $url, PHP_URL_HOST);
7941
7942
		if (strpos($host, '[') === 0 && strpos($host, ']') === strlen($host) - 1 && strpos($host, ':') !== false)
7943
			$url = str_replace($host, '127.0.0.1', $url);
7944
	}
7945
7946
	if (filter_var($url, FILTER_VALIDATE_URL, $flags) !== false)
7947
		return $iri;
7948
	else
7949
		return false;
7950
}
7951
7952
/**
7953
 * A wrapper for `filter_var($url, FILTER_SANITIZE_URL)` that can handle URLs
7954
 * with international characters (a.k.a. IRIs)
7955
 *
7956
 * Note: The returned value will still be an IRI, not a URL. To convert to URL,
7957
 * feed the result of this function to iri_to_url()
7958
 *
7959
 * @param string $iri The IRI to sanitize.
7960
 * @return string|bool The sanitized version of the IRI
7961
 */
7962
function sanitize_iri($iri)
7963
{
7964
	// Encode any non-ASCII characters (but not space or control characters of any sort)
7965
	// Also encode '%' in order to preserve anything that is already percent-encoded.
7966
	$iri = preg_replace_callback(
7967
		'~[^\x00-\x7F\pZ\pC]|%~u',
7968
		function($matches)
7969
		{
7970
			return rawurlencode($matches[0]);
7971
		},
7972
		$iri
7973
	);
7974
7975
	// Perform normal sanitization
7976
	$iri = filter_var($iri, FILTER_SANITIZE_URL);
7977
7978
	// Decode the non-ASCII characters
7979
	$iri = rawurldecode($iri);
7980
7981
	return $iri;
7982
}
7983
7984
/**
7985
 * Performs Unicode normalization on IRIs.
7986
 *
7987
 * Internally calls sanitize_iri(), then performs Unicode normalization on the
7988
 * IRI as a whole, using NFKC normalization for the domain name (see RFC 3491)
7989
 * and NFC normalization for the rest.
7990
 *
7991
 * @param string $iri The IRI to normalize.
7992
 * @return string|bool The normalized version of the IRI.
7993
 */
7994
function normalize_iri($iri)
7995
{
7996
	global $sourcedir, $context, $txt, $db_character_set;
7997
7998
	// If we are not using UTF-8, just sanitize and return.
7999
	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')))
8000
		return sanitize_iri($iri);
8001
8002
	require_once($sourcedir . '/Subs-Charset.php');
8003
8004
	$iri = sanitize_iri(utf8_normalize_c($iri));
8005
8006
	$host = parse_iri((strpos($iri, '//') === 0 ? 'http:' : '') . $iri, PHP_URL_HOST);
8007
8008
	if (!empty($host))
8009
	{
8010
		$normalized_host = utf8_normalize_kc_casefold($host);
8011
		$pos = strpos($iri, $host);
8012
	}
8013
	else
8014
	{
8015
		$host = '';
8016
		$normalized_host = '';
8017
		$pos = 0;
8018
	}
8019
8020
	$before_host = substr($iri, 0, $pos);
8021
	$after_host = substr($iri, $pos + strlen($host));
8022
8023
	return $before_host . $normalized_host . $after_host;
8024
}
8025
8026
/**
8027
 * Converts a URL with international characters (an IRI) into a pure ASCII URL
8028
 *
8029
 * Uses Punycode to encode any non-ASCII characters in the domain name, and uses
8030
 * standard URL encoding on the rest.
8031
 *
8032
 * @param string $iri A IRI that may or may not contain non-ASCII characters.
8033
 * @return string|bool The URL version of the IRI.
8034
 */
8035
function iri_to_url($iri)
8036
{
8037
	global $sourcedir, $context, $txt, $db_character_set;
8038
8039
	// Sanity check: must be using UTF-8 to do this.
8040
	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')))
8041
		return $iri;
8042
8043
	require_once($sourcedir . '/Subs-Charset.php');
8044
8045
	$iri = sanitize_iri(utf8_normalize_c($iri));
8046
8047
	$host = parse_iri((strpos($iri, '//') === 0 ? 'http:' : '') . $iri, PHP_URL_HOST);
8048
8049
	if (!empty($host))
8050
	{
8051
		if (!function_exists('idn_to_ascii'))
8052
			require_once($sourcedir . '/Subs-Compat.php');
8053
8054
		// Convert the host using the Punycode algorithm
8055
		$encoded_host = idn_to_ascii($host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
8056
8057
		$pos = strpos($iri, $host);
8058
	}
8059
	else
8060
	{
8061
		$host = '';
8062
		$encoded_host = '';
8063
		$pos = 0;
8064
	}
8065
8066
	$before_host = substr($iri, 0, $pos);
8067
	$after_host = substr($iri, $pos + strlen($host));
8068
8069
	// Encode any disallowed characters in the rest of the URL
8070
	$unescaped = array(
8071
		'%21' => '!', '%23' => '#', '%24' => '$', '%26' => '&',
8072
		'%27' => "'", '%28' => '(', '%29' => ')', '%2A' => '*',
8073
		'%2B' => '+', '%2C' => ',', '%2F' => '/', '%3A' => ':',
8074
		'%3B' => ';', '%3D' => '=', '%3F' => '?', '%40' => '@',
8075
		'%25' => '%',
8076
	);
8077
8078
	$before_host = strtr(rawurlencode($before_host), $unescaped);
8079
	$after_host = strtr(rawurlencode($after_host), $unescaped);
8080
8081
	return $before_host . $encoded_host . $after_host;
8082
}
8083
8084
/**
8085
 * Decodes a URL containing encoded international characters to UTF-8
8086
 *
8087
 * Decodes any Punycode encoded characters in the domain name, then uses
8088
 * standard URL decoding on the rest.
8089
 *
8090
 * @param string $url The pure ASCII version of a URL.
8091
 * @return string|bool The UTF-8 version of the URL.
8092
 */
8093
function url_to_iri($url)
8094
{
8095
	global $sourcedir, $context, $txt, $db_character_set;
8096
8097
	// Sanity check: must be using UTF-8 to do this.
8098
	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')))
8099
		return $url;
8100
8101
	$host = parse_iri((strpos($url, '//') === 0 ? 'http:' : '') . $url, PHP_URL_HOST);
8102
8103
	if (!empty($host))
8104
	{
8105
		if (!function_exists('idn_to_utf8'))
8106
			require_once($sourcedir . '/Subs-Compat.php');
8107
8108
		// Decode the domain from Punycode
8109
		$decoded_host = idn_to_utf8($host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
8110
8111
		$pos = strpos($url, $host);
8112
	}
8113
	else
8114
	{
8115
		$decoded_host = '';
8116
		$pos = 0;
8117
	}
8118
8119
	$before_host = substr($url, 0, $pos);
8120
	$after_host = substr($url, $pos + strlen($host));
8121
8122
	// Decode the rest of the URL, but preserve escaped URL syntax characters.
8123
	$double_escaped = array(
8124
		'%21' => '%2521', '%23' => '%2523', '%24' => '%2524', '%26' => '%2526',
8125
		'%27' => '%2527', '%28' => '%2528', '%29' => '%2529', '%2A' => '%252A',
8126
		'%2B' => '%252B', '%2C' => '%252C', '%2F' => '%252F', '%3A' => '%253A',
8127
		'%3B' => '%253B', '%3D' => '%253D', '%3F' => '%253F', '%40' => '%2540',
8128
		'%25' => '%2525',
8129
	);
8130
8131
	$before_host = rawurldecode(strtr($before_host, $double_escaped));
8132
	$after_host = rawurldecode(strtr($after_host, $double_escaped));
8133
8134
	return $before_host . $decoded_host . $after_host;
8135
}
8136
8137
/**
8138
 * Ensures SMF's scheduled tasks are being run as intended
8139
 *
8140
 * If the admin activated the cron_is_real_cron setting, but the cron job is
8141
 * not running things at least once per day, we need to go back to SMF's default
8142
 * behaviour using "web cron" JavaScript calls.
8143
 */
8144
function check_cron()
8145
{
8146
	global $modSettings, $smcFunc, $txt;
8147
8148
	if (!empty($modSettings['cron_is_real_cron']) && time() - @intval($modSettings['cron_last_checked']) > 84600)
8149
	{
8150
		$request = $smcFunc['db_query']('', '
8151
			SELECT COUNT(*)
8152
			FROM {db_prefix}scheduled_tasks
8153
			WHERE disabled = {int:not_disabled}
8154
				AND next_time < {int:yesterday}',
8155
			array(
8156
				'not_disabled' => 0,
8157
				'yesterday' => time() - 84600,
8158
			)
8159
		);
8160
		list($overdue) = $smcFunc['db_fetch_row']($request);
8161
		$smcFunc['db_free_result']($request);
8162
8163
		// If we have tasks more than a day overdue, cron isn't doing its job.
8164
		if (!empty($overdue))
8165
		{
8166
			loadLanguage('ManageScheduledTasks');
8167
			log_error($txt['cron_not_working']);
8168
			updateSettings(array('cron_is_real_cron' => 0));
8169
		}
8170
		else
8171
			updateSettings(array('cron_last_checked' => time()));
8172
	}
8173
}
8174
8175
/**
8176
 * Sends an appropriate HTTP status header based on a given status code
8177
 *
8178
 * @param int $code The status code
8179
 * @param string $status The string for the status. Set automatically if not provided.
8180
 */
8181
function send_http_status($code, $status = '')
8182
{
8183
	global $sourcedir;
8184
8185
	// This will fail anyways if headers have been sent.
8186
	if (headers_sent())
8187
		return;
8188
8189
	$statuses = array(
8190
		204 => 'No Content',
8191
		206 => 'Partial Content',
8192
		304 => 'Not Modified',
8193
		400 => 'Bad Request',
8194
		403 => 'Forbidden',
8195
		404 => 'Not Found',
8196
		410 => 'Gone',
8197
		500 => 'Internal Server Error',
8198
		503 => 'Service Unavailable',
8199
	);
8200
8201
	$protocol = !empty($_SERVER['SERVER_PROTOCOL']) && preg_match('~^\s*(HTTP/[12]\.\d)\s*$~i', $_SERVER['SERVER_PROTOCOL'], $matches) ? $matches[1] : 'HTTP/1.0';
8202
8203
	// Typically during these requests, we have cleaned the response (ob_*clean), ensure these headers exist.
8204
	require_once($sourcedir . '/Security.php');
8205
	frameOptionsHeader();
8206
	corsPolicyHeader();
8207
8208
	if (!isset($statuses[$code]) && empty($status))
8209
		header($protocol . ' 500 Internal Server Error');
8210
	else
8211
		header($protocol . ' ' . $code . ' ' . (!empty($status) ? $status : $statuses[$code]));
8212
}
8213
8214
/**
8215
 * Concatenates an array of strings into a grammatically correct sentence list
8216
 *
8217
 * Uses formats defined in the language files to build the list appropropriately
8218
 * for the currently loaded language.
8219
 *
8220
 * @param array $list An array of strings to concatenate.
8221
 * @return string The localized sentence list.
8222
 */
8223
function sentence_list($list)
8224
{
8225
	global $txt;
8226
8227
	// Make sure the bare necessities are defined
8228
	if (empty($txt['sentence_list_format']['n']))
8229
		$txt['sentence_list_format']['n'] = '{series}';
8230
	if (!isset($txt['sentence_list_separator']))
8231
		$txt['sentence_list_separator'] = ', ';
8232
	if (!isset($txt['sentence_list_separator_alt']))
8233
		$txt['sentence_list_separator_alt'] = '; ';
8234
8235
	// Which format should we use?
8236
	if (isset($txt['sentence_list_format'][count($list)]))
8237
		$format = $txt['sentence_list_format'][count($list)];
8238
	else
8239
		$format = $txt['sentence_list_format']['n'];
8240
8241
	// Do we want the normal separator or the alternate?
8242
	$separator = $txt['sentence_list_separator'];
8243
	foreach ($list as $item)
8244
	{
8245
		if (strpos($item, $separator) !== false)
8246
		{
8247
			$separator = $txt['sentence_list_separator_alt'];
8248
			$format = strtr($format, trim($txt['sentence_list_separator']), trim($separator));
8249
			break;
8250
		}
8251
	}
8252
8253
	$replacements = array();
8254
8255
	// Special handling for the last items on the list
8256
	$i = 0;
8257
	while (empty($done))
8258
	{
8259
		if (strpos($format, '{'. --$i . '}') !== false)
8260
			$replacements['{'. $i . '}'] = array_pop($list);
8261
		else
8262
			$done = true;
8263
	}
8264
	unset($done);
8265
8266
	// Special handling for the first items on the list
8267
	$i = 0;
8268
	while (empty($done))
8269
	{
8270
		if (strpos($format, '{'. ++$i . '}') !== false)
8271
			$replacements['{'. $i . '}'] = array_shift($list);
8272
		else
8273
			$done = true;
8274
	}
8275
	unset($done);
8276
8277
	// Whatever is left
8278
	$replacements['{series}'] = implode($separator, $list);
8279
8280
	// Do the deed
8281
	return strtr($format, $replacements);
8282
}
8283
8284
/**
8285
 * Truncate an array to a specified length
8286
 *
8287
 * @param array $array The array to truncate
8288
 * @param int $max_length The upperbound on the length
8289
 * @param int $deep How levels in an multidimensional array should the function take into account.
8290
 * @return array The truncated array
8291
 */
8292
function truncate_array($array, $max_length = 1900, $deep = 3)
8293
{
8294
	$array = (array) $array;
8295
8296
	$curr_length = array_length($array, $deep);
8297
8298
	if ($curr_length <= $max_length)
8299
		return $array;
8300
8301
	else
8302
	{
8303
		// Truncate each element's value to a reasonable length
8304
		$param_max = floor($max_length / count($array));
8305
8306
		$current_deep = $deep - 1;
8307
8308
		foreach ($array as $key => &$value)
8309
		{
8310
			if (is_array($value))
8311
				if ($current_deep > 0)
8312
					$value = truncate_array($value, $current_deep);
8313
8314
			else
8315
				$value = substr($value, 0, $param_max - strlen($key) - 5);
0 ignored issues
show
$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

8315
				$value = substr($value, 0, /** @scrutinizer ignore-type */ $param_max - strlen($key) - 5);
Loading history...
$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

8315
				$value = substr(/** @scrutinizer ignore-type */ $value, 0, $param_max - strlen($key) - 5);
Loading history...
8316
		}
8317
8318
		return $array;
8319
	}
8320
}
8321
8322
/**
8323
 * array_length Recursive
8324
 * @param array $array
8325
 * @param int $deep How many levels should the function
8326
 * @return int
8327
 */
8328
function array_length($array, $deep = 3)
8329
{
8330
	// Work with arrays
8331
	$array = (array) $array;
8332
	$length = 0;
8333
8334
	$deep_count = $deep - 1;
8335
8336
	foreach ($array as $value)
8337
	{
8338
		// Recursive?
8339
		if (is_array($value))
8340
		{
8341
			// No can't do
8342
			if ($deep_count <= 0)
8343
				continue;
8344
8345
			$length += array_length($value, $deep_count);
8346
		}
8347
		else
8348
			$length += strlen($value);
8349
	}
8350
8351
	return $length;
8352
}
8353
8354
/**
8355
 * Compares existance request variables against an array.
8356
 *
8357
 * The input array is associative, where keys denote accepted values
8358
 * in a request variable denoted by `$req_val`. Values can be:
8359
 *
8360
 * - another associative array where at least one key must be found
8361
 *   in the request and their values are accepted request values.
8362
 * - A scalar value, in which case no furthur checks are done.
8363
 *
8364
 * @param array $array
8365
 * @param string $req_var request variable
8366
 *
8367
 * @return bool whether any of the criteria was satisfied
8368
 */
8369
function is_filtered_request(array $array, $req_var)
8370
{
8371
	$matched = false;
8372
	if (isset($_REQUEST[$req_var], $array[$_REQUEST[$req_var]]))
8373
	{
8374
		if (is_array($array[$_REQUEST[$req_var]]))
8375
		{
8376
			foreach ($array[$_REQUEST[$req_var]] as $subtype => $subnames)
8377
				$matched |= isset($_REQUEST[$subtype]) && in_array($_REQUEST[$subtype], $subnames);
8378
		}
8379
		else
8380
			$matched = true;
8381
	}
8382
8383
	return (bool) $matched;
8384
}
8385
8386
/**
8387
 * Clean up the XML to make sure it doesn't contain invalid characters.
8388
 *
8389
 * See https://www.w3.org/TR/xml/#charsets
8390
 *
8391
 * @param string $string The string to clean
8392
 * @return string The cleaned string
8393
 */
8394
function cleanXml($string)
8395
{
8396
	global $context;
8397
8398
	$illegal_chars = array(
8399
		// Remove all ASCII control characters except \t, \n, and \r.
8400
		"\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08",
8401
		"\x0B", "\x0C", "\x0E", "\x0F", "\x10", "\x11", "\x12", "\x13", "\x14",
8402
		"\x15", "\x16", "\x17", "\x18", "\x19", "\x1A", "\x1B", "\x1C", "\x1D",
8403
		"\x1E", "\x1F",
8404
		// Remove \xFFFE and \xFFFF
8405
		"\xEF\xBF\xBE", "\xEF\xBF\xBF",
8406
	);
8407
8408
	$string = str_replace($illegal_chars, '', $string);
8409
8410
	// The Unicode surrogate pair code points should never be present in our
8411
	// strings to begin with, but if any snuck in, they need to be removed.
8412
	if (!empty($context['utf8']) && strpos($string, "\xED") !== false)
8413
		$string = preg_replace('/\xED[\xA0-\xBF][\x80-\xBF]/', '', $string);
8414
8415
	return $string;
8416
}
8417
8418
/**
8419
 * Escapes (replaces) characters in strings to make them safe for use in JavaScript
8420
 *
8421
 * @param string $string The string to escape
8422
 * @param bool $as_json If true, escape as double-quoted string. Default false.
8423
 * @return string The escaped string
8424
 */
8425
function JavaScriptEscape($string, $as_json = false)
8426
{
8427
	global $scripturl;
8428
8429
	$q = !empty($as_json) ? '"' : '\'';
8430
8431
	return $q . strtr($string, array(
8432
		"\r" => '',
8433
		"\n" => '\\n',
8434
		"\t" => '\\t',
8435
		'\\' => '\\\\',
8436
		$q => addslashes($q),
8437
		'</' => '<' . $q . ' + ' . $q . '/',
8438
		'<script' => '<scri' . $q . '+' . $q . 'pt',
8439
		'<body>' => '<bo' . $q . '+' . $q . 'dy>',
8440
		'<a href' => '<a hr' . $q . '+' . $q . 'ef',
8441
		$scripturl => $q . ' + smf_scripturl + ' . $q,
8442
	)) . $q;
8443
}
8444
8445
function tokenTxtReplace($stringSubject = '')
8446
{
8447
	global $txt;
8448
8449
	if (empty($stringSubject))
8450
		return '';
8451
8452
	$translatable_tokens = preg_match_all('/{(.*?)}/' , $stringSubject, $matches);
0 ignored issues
show
The assignment to $translatable_tokens is dead and can be removed.
Loading history...
8453
	$toFind = array();
8454
	$replaceWith = array();
8455
8456
	if (!empty($matches[1]))
8457
		foreach ($matches[1] as $token) {
8458
			$toFind[] = '{' . $token . '}';
8459
			$replaceWith[] = isset($txt[$token]) ? $txt[$token] : $token;
8460
		}
8461
8462
	return str_replace($toFind, $replaceWith, $stringSubject);
8463
}
8464
8465
?>