redirectexit()   F
last analyzed

Complexity

Conditions 22
Paths 216

Size

Total Lines 54
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 22
eloc 28
nc 216
nop 3
dl 0
loc 54
rs 3.1333
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file has all the main functions in it that relate to, well, everything.
5
 *
6
 * Simple Machines Forum (SMF)
7
 *
8
 * @package SMF
9
 * @author Simple Machines https://www.simplemachines.org
10
 * @copyright 2022 Simple Machines and individual contributors
11
 * @license https://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1.2
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 = max(0, $start);
570
	$start = min($start - ($start % $num_per_page), $max_value);
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
Bug introduced by
It seems like (double)$number === $num...rride_decimal_count : 0 can also be of type true; however, parameter $decimals of number_format() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

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
	// Ensure required values are set
720
	$user_info['time_format'] = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %H:%M');
721
722
	// For backward compatibility, replace empty values with user's time zone
723
	// and replace 'forum' with forum's default time zone.
724
	$tzid = empty($tzid) ? getUserTimezone() : (($tzid === 'forum' || @timezone_open((string) $tzid) === false) ? $modSettings['default_timezone'] : (string) $tzid);
725
726
	// Today and Yesterday?
727
	$prefix = '';
728
	if ($modSettings['todayMod'] >= 1 && $show_today === true)
729
	{
730
		if (!isset($today[$tzid]))
731
			$today[$tzid] = date_format(date_create('today ' . $tzid), 'U');
732
733
		// Tomorrow? We don't support the future. ;)
734
		if ($log_time >= $today[$tzid] + 86400)
735
		{
736
			$prefix = '';
737
		}
738
		// Today.
739
		elseif ($log_time >= $today[$tzid])
740
		{
741
			$prefix = $txt['today'];
742
		}
743
		// Yesterday.
744
		elseif ($modSettings['todayMod'] > 1 && $log_time >= $today[$tzid] - 86400)
745
		{
746
			$prefix = $txt['yesterday'];
747
		}
748
	}
749
750
	// 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.
751
	$format = !is_bool($show_today) ? $show_today : $user_info['time_format'];
752
753
	$format = !empty($prefix) ? get_date_or_time_format('time', $format) : $format;
754
755
	// And now, the moment we've all be waiting for...
756
	return $prefix . smf_strftime($format, $log_time, $tzid);
757
}
758
759
/**
760
 * Gets a version of a strftime() format that only shows the date or time components
761
 *
762
 * @param string $type Either 'date' or 'time'.
763
 * @param string $format A strftime() format to process. Defaults to $user_info['time_format'].
764
 * @return string A strftime() format string
765
 */
766
function get_date_or_time_format($type = '', $format = '')
767
{
768
	global $user_info, $modSettings;
769
	static $formats;
770
771
	// If the format is invalid, fall back to defaults.
772
	if (strpos($format, '%') === false)
773
		$format = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %k:%M');
774
775
	$orig_format = $format;
776
777
	// Have we already done this?
778
	if (isset($formats[$orig_format][$type]))
779
		return $formats[$orig_format][$type];
780
781
	if ($type === 'date')
782
	{
783
		$specifications = array(
784
			// Day
785
			'%a' => '%a', '%A' => '%A', '%e' => '%e', '%d' => '%d', '%j' => '%j', '%u' => '%u', '%w' => '%w',
786
			// Week
787
			'%U' => '%U', '%V' => '%V', '%W' => '%W',
788
			// Month
789
			'%b' => '%b', '%B' => '%B', '%h' => '%h', '%m' => '%m',
790
			// Year
791
			'%C' => '%C', '%g' => '%g', '%G' => '%G', '%y' => '%y', '%Y' => '%Y',
792
			// Time
793
			'%H' => '', '%k' => '', '%I' => '', '%l' => '', '%M' => '', '%p' => '', '%P' => '',
794
			'%r' => '', '%R' => '', '%S' => '', '%T' => '', '%X' => '', '%z' => '', '%Z' => '',
795
			// Time and Date Stamps
796
			'%c' => '%x', '%D' => '%D', '%F' => '%F', '%s' => '%s', '%x' => '%x',
797
			// Miscellaneous
798
			'%n' => '', '%t' => '', '%%' => '%%',
799
		);
800
801
		$default_format = '%F';
802
	}
803
	elseif ($type === 'time')
804
	{
805
		$specifications = array(
806
			// Day
807
			'%a' => '', '%A' => '', '%e' => '', '%d' => '', '%j' => '', '%u' => '', '%w' => '',
808
			// Week
809
			'%U' => '', '%V' => '', '%W' => '',
810
			// Month
811
			'%b' => '', '%B' => '', '%h' => '', '%m' => '',
812
			// Year
813
			'%C' => '', '%g' => '', '%G' => '', '%y' => '', '%Y' => '',
814
			// Time
815
			'%H' => '%H', '%k' => '%k', '%I' => '%I', '%l' => '%l', '%M' => '%M', '%p' => '%p', '%P' => '%P',
816
			'%r' => '%r', '%R' => '%R', '%S' => '%S', '%T' => '%T', '%X' => '%X', '%z' => '%z', '%Z' => '%Z',
817
			// Time and Date Stamps
818
			'%c' => '%X', '%D' => '', '%F' => '', '%s' => '%s', '%x' => '',
819
			// Miscellaneous
820
			'%n' => '', '%t' => '', '%%' => '%%',
821
		);
822
823
		$default_format = '%k:%M';
824
	}
825
	// Invalid type requests just get the full format string.
826
	else
827
		return $format;
828
829
	// Separate the specifications we want from the ones we don't.
830
	$wanted = array_filter($specifications);
831
	$unwanted = array_diff(array_keys($specifications), $wanted);
832
833
	// First, make any necessary substitutions in the format.
834
	$format = strtr($format, $wanted);
835
836
	// Next, strip out any specifications and literal text that we don't want.
837
	$format_parts = preg_split('~%[' . (strtr(implode('', $unwanted), array('%' => ''))) . ']~u', $format);
838
839
	foreach ($format_parts as $p => $f)
840
	{
841
		if (strpos($f, '%') === false)
842
			unset($format_parts[$p]);
843
	}
844
845
	$format = implode('', $format_parts);
846
847
	// Finally, strip out any unwanted leftovers.
848
	// 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
849
	$format = preg_replace(
850
		array(
851
			// Anything that isn't a specification, punctuation mark, or whitespace.
852
			'~(?<!%)\p{L}|[^\p{L}\p{P}\s]~u',
853
			// Repeated punctuation marks (except %), possibly separated by whitespace.
854
			'~(?'.'>([^%\P{P}])\s*(?=\1))*~u',
855
			'~([^%\P{P}])(?'.'>\1(?!$))*~u',
856
			// Unwanted trailing punctuation and whitespace.
857
			'~(?'.'>([\p{Pd}\p{Ps}\p{Pi}\p{Pc}]|[^%\P{Po}])\s*)*$~u',
858
			// Unwanted opening punctuation and whitespace.
859
			'~^\s*(?'.'>([\p{Pd}\p{Pe}\p{Pf}\p{Pc}]|[^%\P{Po}])\s*)*~u',
860
			// Runs of horizontal whitespace.
861
			'~\s+~',
862
		),
863
		array(
864
			'',
865
			'$1',
866
			'$1$2',
867
			'',
868
			'',
869
			' ',
870
		),
871
		$format
872
	);
873
874
	// Gotta have something...
875
	if (empty($format))
876
		$format = $default_format;
877
878
	// Remember what we've done.
879
	$formats[$orig_format][$type] = trim($format);
880
881
	return $formats[$orig_format][$type];
882
}
883
884
/**
885
 * Replacement for strftime() that is compatible with PHP 8.1+.
886
 *
887
 * This does not use the system's strftime library or locale setting,
888
 * so results may vary in a few cases from the results of strftime():
889
 *
890
 *  - %a, %A, %b, %B, %p, %P: Output will use SMF's language strings
891
 *    to localize these values. If SMF's language strings have not
892
 *    been loaded, PHP's default English strings will be used.
893
 *
894
 *  - %c, %x, %X: Output will always use ISO format.
895
 *
896
 * @param string $format A strftime() format string.
897
 * @param int|null $timestamp A Unix timestamp.
898
 *     If null, defaults to the current time.
899
 * @param string|null $tzid Time zone identifier.
900
 *     If null, uses default time zone.
901
 * @return string The formatted datetime string.
902
 */
903
function smf_strftime(string $format, int $timestamp = null, string $tzid = null)
904
{
905
	global $txt, $smcFunc, $sourcedir;
906
907
	static $dates = array();
908
909
	// Set default values as necessary.
910
	if (!isset($timestamp))
911
		$timestamp = time();
912
913
	if (!isset($tzid))
914
		$tzid = date_default_timezone_get();
915
916
	// A few substitutions to make life easier.
917
	$format = strtr($format, array(
918
		'%h' => '%b',
919
		'%r' => '%I:%M:%S %p',
920
		'%R' => '%H:%M',
921
		'%T' => '%H:%M:%S',
922
		'%X' => '%H:%M:%S',
923
		'%D' => '%m/%d/%y',
924
		'%F' => '%Y-%m-%d',
925
		'%x' => '%Y-%m-%d',
926
	));
927
928
	// Avoid unnecessary repetition.
929
	if (isset($dates[$tzid . '_' . $timestamp]['results'][$format]))
930
		return $dates[$tzid . '_' . $timestamp]['results'][$format];
931
932
	// Ensure the TZID is valid.
933
	if (($tz = @timezone_open($tzid)) === false)
934
	{
935
		$tzid = date_default_timezone_get();
936
937
		// Check again now that we have a valid TZID.
938
		if (isset($dates[$tzid . '_' . $timestamp]['results'][$format]))
939
			return $dates[$tzid . '_' . $timestamp]['results'][$format];
940
941
		$tz = timezone_open($tzid);
942
	}
943
944
	// Create the DateTime object and set its time zone.
945
	if (!isset($dates[$tzid . '_' . $timestamp]['object']))
946
	{
947
		$dates[$tzid . '_' . $timestamp]['object'] = date_create('@' . $timestamp);
948
		date_timezone_set($dates[$tzid . '_' . $timestamp]['object'], $tz);
949
	}
950
951
	// In case this function is called before reloadSettings().
952
	if (!isset($smcFunc['strtoupper']))
953
	{
954
		if (function_exists('mb_strtoupper'))
955
		{
956
			$smcFunc['strtoupper'] = 'mb_strtoupper';
957
			$smcFunc['strtolower'] = 'mb_strtolower';
958
		}
959
		elseif (isset($sourcedir))
960
		{
961
			require_once($sourcedir . '/Subs-Charset.php');
962
			$smcFunc['strtoupper'] = 'utf8_strtoupper';
963
			$smcFunc['strtolower'] = 'utf8_strtolower';
964
		}
965
		else
966
		{
967
			$smcFunc['strtoupper'] = 'strtoupper';
968
			$smcFunc['strtolower'] = 'strtolower';
969
		}
970
	}
971
972
	$format_equivalents = array(
973
		// Day
974
		'a' => 'D', // Complex: prefer $txt strings if available.
975
		'A' => 'l', // Complex: prefer $txt strings if available.
976
		'e' => 'j', // Complex: sprintf to prepend whitespace.
977
		'd' => 'd',
978
		'j' => 'z', // Complex: must add one and then sprintf to prepend zeros.
979
		'u' => 'N',
980
		'w' => 'w',
981
		// Week
982
		'U' => 'z_w_0', // Complex: calculated from these other values.
983
		'V' => 'W',
984
		'W' => 'z_w_1', // Complex: calculated from these other values.
985
		// Month
986
		'b' => 'M', // Complex: prefer $txt strings if available.
987
		'B' => 'F', // Complex: prefer $txt strings if available.
988
		'm' => 'm',
989
		// Year
990
		'C' => 'Y', // Complex: Get 'Y' then truncate to first two digits.
991
		'g' => 'o', // Complex: Get 'o' then truncate to last two digits.
992
		'G' => 'o', // Complex: Get 'o' then sprintf to ensure four digits.
993
		'y' => 'y',
994
		'Y' => 'Y',
995
		// Time
996
		'H' => 'H',
997
		'k' => 'G',
998
		'I' => 'h',
999
		'l' => 'g', // Complex: sprintf to prepend whitespace.
1000
		'M' => 'i',
1001
		'p' => 'A', // Complex: prefer $txt strings if available.
1002
		'P' => 'a', // Complex: prefer $txt strings if available.
1003
		'S' => 's',
1004
		'z' => 'O',
1005
		'Z' => 'T',
1006
		// Time and Date Stamps
1007
		'c' => 'c',
1008
		's' => 'U',
1009
		// Miscellaneous
1010
		'n' => "\n",
1011
		't' => "\t",
1012
		'%' => '%',
1013
	);
1014
1015
	// Translate from strftime format to DateTime format.
1016
	$parts = preg_split('/%(' . implode('|', array_keys($format_equivalents)) . ')/', $format, 0, PREG_SPLIT_DELIM_CAPTURE);
1017
1018
	$placeholders = array();
1019
	$complex = false;
1020
1021
	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...
1022
	{
1023
		// Parts that are not strftime formats.
1024
		if ($i % 2 === 0 || !isset($format_equivalents[$parts[$i]]))
1025
		{
1026
			if ($parts[$i] === '')
1027
				continue;
1028
1029
			$placeholder = "\xEE\x84\x80" . $i . "\xEE\x84\x81";
1030
1031
			$placeholders[$placeholder] = $parts[$i];
1032
			$parts[$i] = $placeholder;
1033
		}
1034
		// Parts that need localized strings.
1035
		elseif (in_array($parts[$i], array('a', 'A', 'b', 'B')))
1036
		{
1037
			switch ($parts[$i])
1038
			{
1039
				case 'a':
1040
					$min = 0;
1041
					$max = 6;
1042
					$key = 'days_short';
1043
					$f = 'w';
1044
					$placeholder_end = "\xEE\x84\x83";
1045
1046
					break;
1047
1048
				case 'A':
1049
					$min = 0;
1050
					$max = 6;
1051
					$key = 'days';
1052
					$f = 'w';
1053
					$placeholder_end = "\xEE\x84\x82";
1054
1055
					break;
1056
1057
				case 'b':
1058
					$min = 1;
1059
					$max = 12;
1060
					$key = 'months_short';
1061
					$f = 'n';
1062
					$placeholder_end = "\xEE\x84\x85";
1063
1064
					break;
1065
1066
				case 'B':
1067
					$min = 1;
1068
					$max = 12;
1069
					$key = 'months';
1070
					$f = 'n';
1071
					$placeholder_end = "\xEE\x84\x84";
1072
1073
					break;
1074
			}
1075
1076
			$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...
1077
1078
			// Check whether $txt contains all expected strings.
1079
			// If not, use English default.
1080
			$txt_strings_exist = true;
1081
			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...
1082
			{
1083
				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...
1084
				{
1085
					$txt_strings_exist = false;
1086
					break;
1087
				}
1088
				else
1089
					$placeholders[str_replace($f, $num, $placeholder)] = $txt[$key][$num];
1090
			}
1091
1092
			$parts[$i] = $txt_strings_exist ? $placeholder : $format_equivalents[$parts[$i]];
1093
		}
1094
		elseif (in_array($parts[$i], array('p', 'P')))
1095
		{
1096
			if (!isset($txt['time_am']) || !isset($txt['time_pm']))
1097
				continue;
1098
1099
			$placeholder = "\xEE\x84\x90" . $format_equivalents[$parts[$i]] . "\xEE\x84\x91";
1100
1101
			switch ($parts[$i])
1102
			{
1103
				// Lower case
1104
				case 'p':
1105
					$placeholders[str_replace($format_equivalents[$parts[$i]], 'AM', $placeholder)] = $smcFunc['strtoupper']($txt['time_am']);
1106
					$placeholders[str_replace($format_equivalents[$parts[$i]], 'PM', $placeholder)] = $smcFunc['strtoupper']($txt['time_pm']);
1107
					break;
1108
1109
				// Upper case
1110
				case 'P':
1111
					$placeholders[str_replace($format_equivalents[$parts[$i]], 'am', $placeholder)] = $smcFunc['strtolower']($txt['time_am']);
1112
					$placeholders[str_replace($format_equivalents[$parts[$i]], 'pm', $placeholder)] = $smcFunc['strtolower']($txt['time_pm']);
1113
					break;
1114
			}
1115
1116
			$parts[$i] = $placeholder;
1117
		}
1118
		// Parts that will need further processing.
1119
		elseif (in_array($parts[$i], array('j', 'C', 'U', 'W', 'G', 'g', 'e', 'l')))
1120
		{
1121
			$complex = true;
1122
1123
			switch ($parts[$i])
1124
			{
1125
				case 'j':
1126
					$placeholder_end = "\xEE\x84\xA1";
1127
					break;
1128
1129
				case 'C':
1130
					$placeholder_end = "\xEE\x84\xA2";
1131
					break;
1132
1133
				case 'U':
1134
				case 'W':
1135
					$placeholder_end = "\xEE\x84\xA3";
1136
					break;
1137
1138
				case 'G':
1139
					$placeholder_end = "\xEE\x84\xA4";
1140
					break;
1141
1142
				case 'g':
1143
					$placeholder_end = "\xEE\x84\xA5";
1144
					break;
1145
1146
				case 'e':
1147
				case 'l':
1148
					$placeholder_end = "\xEE\x84\xA6";
1149
			}
1150
1151
			$parts[$i] = "\xEE\x84\xA0" . $format_equivalents[$parts[$i]] . $placeholder_end;
1152
		}
1153
		// Parts with simple equivalents.
1154
		else
1155
			$parts[$i] = $format_equivalents[$parts[$i]];
1156
	}
1157
1158
	// The main event.
1159
	$dates[$tzid . '_' . $timestamp]['results'][$format] = strtr(date_format($dates[$tzid . '_' . $timestamp]['object'], implode('', $parts)), $placeholders);
1160
1161
	// Deal with the complicated ones.
1162
	if ($complex)
0 ignored issues
show
introduced by
The condition $complex is always false.
Loading history...
1163
	{
1164
		$dates[$tzid . '_' . $timestamp]['results'][$format] = preg_replace_callback(
1165
			'/\xEE\x84\xA0([\d_]+)(\xEE\x84(?:[\xA1-\xAF]))/',
1166
			function ($matches)
1167
			{
1168
				switch ($matches[2])
1169
				{
1170
					// %j
1171
					case "\xEE\x84\xA1":
1172
						$replacement = sprintf('%03d', (int) $matches[1] + 1);
1173
						break;
1174
1175
					// %C
1176
					case "\xEE\x84\xA2":
1177
						$replacement = substr(sprintf('%04d', $matches[1]), 0, 2);
1178
						break;
1179
1180
					// %U and %W
1181
					case "\xEE\x84\xA3":
1182
						list($day_of_year, $day_of_week, $first_day) = explode('_', $matches[1]);
1183
						$replacement = sprintf('%02d', floor(((int) $day_of_year - (int) $day_of_week + (int) $first_day) / 7) + 1);
1184
						break;
1185
1186
					// %G
1187
					case "\xEE\x84\xA4":
1188
						$replacement = sprintf('%04d', $matches[1]);
1189
						break;
1190
1191
					// %g
1192
					case "\xEE\x84\xA5":
1193
						$replacement = substr(sprintf('%04d', $matches[1]), -2);
1194
						break;
1195
1196
					// %e and %l
1197
					case "\xEE\x84\xA6":
1198
						$replacement = sprintf('%2d', $matches[1]);
1199
						break;
1200
1201
					// Shouldn't happen, but just in case...
1202
					default:
1203
						$replacement = $matches[1];
1204
						break;
1205
				}
1206
1207
				return $replacement;
1208
			},
1209
			$dates[$tzid . '_' . $timestamp]['results'][$format]
1210
		);
1211
	}
1212
1213
	return $dates[$tzid . '_' . $timestamp]['results'][$format];
1214
}
1215
1216
/**
1217
 * Replacement for gmstrftime() that is compatible with PHP 8.1+.
1218
 *
1219
 * Calls smf_strftime() with the $tzid parameter set to 'UTC'.
1220
 *
1221
 * @param string $format A strftime() format string.
1222
 * @param int|null $timestamp A Unix timestamp.
1223
 *     If null, defaults to the current time.
1224
 * @return string The formatted datetime string.
1225
 */
1226
function smf_gmstrftime(string $format, int $timestamp = null)
1227
{
1228
	return smf_strftime($format, $timestamp, 'UTC');
1229
}
1230
1231
/**
1232
 * Replaces special entities in strings with the real characters.
1233
 *
1234
 * Functionally equivalent to htmlspecialchars_decode(), except that this also
1235
 * replaces '&nbsp;' with a simple space character.
1236
 *
1237
 * @param string $string A string
1238
 * @return string The string without entities
1239
 */
1240
function un_htmlspecialchars($string)
1241
{
1242
	global $context;
1243
	static $translation = array();
1244
1245
	// Determine the character set... Default to UTF-8
1246
	if (empty($context['character_set']))
1247
		$charset = 'UTF-8';
1248
	// Use ISO-8859-1 in place of non-supported ISO-8859 charsets...
1249
	elseif (strpos($context['character_set'], 'ISO-8859-') !== false && !in_array($context['character_set'], array('ISO-8859-5', 'ISO-8859-15')))
1250
		$charset = 'ISO-8859-1';
1251
	else
1252
		$charset = $context['character_set'];
1253
1254
	if (empty($translation))
1255
		$translation = array_flip(get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES, $charset)) + array('&#039;' => '\'', '&#39;' => '\'', '&nbsp;' => ' ');
1256
1257
	return strtr($string, $translation);
1258
}
1259
1260
/**
1261
 * Replaces invalid characters with a substitute.
1262
 *
1263
 * !!! Warning !!! Setting $substitute to '' in order to delete invalid
1264
 * characters from the string can create unexpected security problems. See
1265
 * https://www.unicode.org/reports/tr36/#Deletion_of_Noncharacters for an
1266
 * explanation.
1267
 *
1268
 * @param string $string The string to sanitize.
1269
 * @param int $level Controls filtering of invisible formatting characters.
1270
 *      0: Allow valid formatting characters. Use for sanitizing text in posts.
1271
 *      1: Allow necessary formatting characters. Use for sanitizing usernames.
1272
 *      2: Disallow all formatting characters. Use for internal comparisions
1273
 *         only, such as in the word censor, search contexts, etc.
1274
 *      Default: 0.
1275
 * @param string|null $substitute Replacement string for the invalid characters.
1276
 *      If not set, the Unicode replacement character (U+FFFD) will be used
1277
 *      (or a fallback like "?" if necessary).
1278
 * @return string The sanitized string.
1279
 */
1280
function sanitize_chars($string, $level = 0, $substitute = null)
1281
{
1282
	global $context, $sourcedir;
1283
1284
	$string = (string) $string;
1285
	$level = min(max((int) $level, 0), 2);
1286
1287
	// What substitute character should we use?
1288
	if (isset($substitute))
1289
	{
1290
		$substitute = strval($substitute);
1291
	}
1292
	elseif (!empty($context['utf8']))
1293
	{
1294
		// Raw UTF-8 bytes for U+FFFD.
1295
		$substitute = "\xEF\xBF\xBD";
1296
	}
1297
	elseif (!empty($context['character_set']) && is_callable('mb_decode_numericentity'))
1298
	{
1299
		// Get whatever the default replacement character is for this encoding.
1300
		$substitute = mb_decode_numericentity('&#xFFFD;', array(0xFFFD,0xFFFD,0,0xFFFF), $context['character_set']);
1301
	}
1302
	else
1303
		$substitute = '?';
1304
1305
	// Fix any invalid byte sequences.
1306
	if (!empty($context['character_set']))
1307
	{
1308
		// For UTF-8, this preg_match test is much faster than mb_check_encoding.
1309
		$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']));
1310
1311
		if ($malformed)
1312
		{
1313
			// mb_convert_encoding will replace invalid byte sequences with our substitute.
1314
			if (is_callable('mb_convert_encoding'))
1315
			{
1316
				if (!is_callable('mb_ord'))
1317
					require_once($sourcedir . '/Subs-Compat.php');
1318
1319
				$substitute_ord = $substitute === '' ? 'none' : mb_ord($substitute, $context['character_set']);
0 ignored issues
show
Bug introduced by
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

1319
				$substitute_ord = $substitute === '' ? 'none' : mb_ord(/** @scrutinizer ignore-type */ $substitute, $context['character_set']);
Loading history...
1320
1321
				$mb_substitute_character = mb_substitute_character();
1322
				mb_substitute_character($substitute_ord);
1323
1324
				$string = mb_convert_encoding($string, $context['character_set'], $context['character_set']);
1325
1326
				mb_substitute_character($mb_substitute_character);
0 ignored issues
show
Bug introduced by
It seems like $mb_substitute_character can also be of type true; however, parameter $substitute_character of mb_substitute_character() does only seem to accept integer|null|string, maybe add an additional type check? ( Ignorable by Annotation )

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

1326
				mb_substitute_character(/** @scrutinizer ignore-type */ $mb_substitute_character);
Loading history...
1327
			}
1328
			else
1329
				return false;
1330
		}
1331
	}
1332
1333
	// Fix any weird vertical space characters.
1334
	$string = normalize_spaces($string, true);
0 ignored issues
show
Bug introduced by
It seems like $string can also be of type array; however, parameter $string of normalize_spaces() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

Loading history...
1733
				{
1734
					$returnContext = '';
1735
1736
					// BBC or the entire attachments feature is disabled
1737
					if (empty($modSettings['attachmentEnable']) || !empty($disabled['attach']))
1738
						return $data;
1739
1740
					// Save the attach ID.
1741
					$attachID = $params['{id}'];
1742
1743
					// Kinda need this.
1744
					require_once($sourcedir . '/Subs-Attachments.php');
1745
1746
					$currentAttachment = parseAttachBBC($attachID);
1747
1748
					// parseAttachBBC will return a string ($txt key) rather than dying with a fatal_error. Up to you to decide what to do.
1749
					if (is_string($currentAttachment))
1750
						return $data = '<span style="display:inline-block" class="errorbox">' . (!empty($txt[$currentAttachment]) ? $txt[$currentAttachment] : $currentAttachment)  . '</span>';
1751
1752
					// We need a display mode.
1753
					if (empty($params['{display}']))
1754
					{
1755
						// Images, video, and audio are embedded by default.
1756
						if (!empty($currentAttachment['is_image']) || strpos($currentAttachment['mime_type'], 'video/') === 0 || strpos($currentAttachment['mime_type'], 'audio/') === 0)
1757
							$params['{display}'] = 'embed';
1758
						// Anything else shows a link by default.
1759
						else
1760
							$params['{display}'] = 'link';
1761
					}
1762
1763
					// Embedded file.
1764
					if ($params['{display}'] == 'embed')
1765
					{
1766
						$alt = ' alt="' . (!empty($params['{alt}']) ? $params['{alt}'] : $currentAttachment['name']) . '"';
1767
						$title = !empty($data) ? ' title="' . $smcFunc['htmlspecialchars']($data) . '"' : '';
1768
1769
						// Image.
1770
						if (!empty($currentAttachment['is_image']))
1771
						{
1772
							if (empty($params['{width}']) && empty($params['{height}']))
1773
								$returnContext .= '<img src="' . $currentAttachment['href'] . '"' . $alt . $title . ' class="bbc_img">';
1774
							else
1775
							{
1776
								$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"': '';
1777
								$height = !empty($params['{height}']) ? 'height="' . $params['{height}'] . '"' : '';
1778
								$returnContext .= '<img src="' . $currentAttachment['href'] . ';image"' . $alt . $title . $width . $height . ' class="bbc_img resized"/>';
1779
							}
1780
						}
1781
						// Video.
1782
						elseif (strpos($currentAttachment['mime_type'], 'video/') === 0)
1783
						{
1784
							$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"' : '';
1785
							$height = !empty($params['{height}']) ? ' height="' . $params['{height}'] . '"' : '';
1786
1787
							$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>' : '');
1788
						}
1789
						// Audio.
1790
						elseif (strpos($currentAttachment['mime_type'], 'audio/') === 0)
1791
						{
1792
							$width = 'max-width:100%; width: ' . (!empty($params['{width}']) ? $params['{width}'] : '400') . 'px;';
1793
							$height = !empty($params['{height}']) ? 'height: ' . $params['{height}'] . 'px;' : '';
1794
1795
							$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>';
1796
						}
1797
						// Anything else.
1798
						else
1799
						{
1800
							$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"' : '';
1801
							$height = !empty($params['{height}']) ? ' height="' . $params['{height}'] . '"' : '';
1802
1803
							$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>';
1804
						}
1805
					}
1806
1807
					// No image. Show a link.
1808
					else
1809
						$returnContext .= '<a href="' . $currentAttachment['href'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . '</a>';
1810
1811
					// Use this hook to adjust the HTML output of the attach BBCode.
1812
					// If you want to work with the attachment data itself, use one of these:
1813
					// - integrate_pre_parseAttachBBC
1814
					// - integrate_post_parseAttachBBC
1815
					call_integration_hook('integrate_attach_bbc_validate', array(&$returnContext, $currentAttachment, $tag, $data, $disabled, $params));
1816
1817
					// Gotta append what we just did.
1818
					$data = $returnContext;
1819
				},
1820
			),
1821
			array(
1822
				'tag' => 'b',
1823
				'before' => '<b>',
1824
				'after' => '</b>',
1825
			),
1826
			// Legacy (equivalent to [ltr] or [rtl])
1827
			array(
1828
				'tag' => 'bdo',
1829
				'type' => 'unparsed_equals',
1830
				'before' => '<bdo dir="$1">',
1831
				'after' => '</bdo>',
1832
				'test' => '(rtl|ltr)\]',
1833
				'block_level' => true,
1834
			),
1835
			// Legacy (alias of [color=black])
1836
			array(
1837
				'tag' => 'black',
1838
				'before' => '<span style="color: black;" class="bbc_color">',
1839
				'after' => '</span>',
1840
			),
1841
			// Legacy (alias of [color=blue])
1842
			array(
1843
				'tag' => 'blue',
1844
				'before' => '<span style="color: blue;" class="bbc_color">',
1845
				'after' => '</span>',
1846
			),
1847
			array(
1848
				'tag' => 'br',
1849
				'type' => 'closed',
1850
				'content' => '<br>',
1851
			),
1852
			array(
1853
				'tag' => 'center',
1854
				'before' => '<div class="centertext">',
1855
				'after' => '</div>',
1856
				'block_level' => true,
1857
			),
1858
			array(
1859
				'tag' => 'code',
1860
				'type' => 'unparsed_content',
1861
				'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>',
1862
				// @todo Maybe this can be simplified?
1863
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1864
				{
1865
					if (!isset($disabled['code']))
1866
					{
1867
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
1868
1869
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
1870
						{
1871
							// Do PHP code coloring?
1872
							if ($php_parts[$php_i] != '&lt;?php')
1873
								continue;
1874
1875
							$php_string = '';
1876
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1877
							{
1878
								$php_string .= $php_parts[$php_i];
1879
								$php_parts[$php_i++] = '';
1880
							}
1881
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1882
						}
1883
1884
						// Fix the PHP code stuff...
1885
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1886
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1887
1888
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1889
						if (!empty($context['browser']['is_opera']))
1890
							$data .= '&nbsp;';
1891
					}
1892
				},
1893
				'block_level' => true,
1894
			),
1895
			array(
1896
				'tag' => 'code',
1897
				'type' => 'unparsed_equals_content',
1898
				'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>',
1899
				// @todo Maybe this can be simplified?
1900
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1901
				{
1902
					if (!isset($disabled['code']))
1903
					{
1904
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1905
1906
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
1907
						{
1908
							// Do PHP code coloring?
1909
							if ($php_parts[$php_i] != '&lt;?php')
1910
								continue;
1911
1912
							$php_string = '';
1913
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1914
							{
1915
								$php_string .= $php_parts[$php_i];
1916
								$php_parts[$php_i++] = '';
1917
							}
1918
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1919
						}
1920
1921
						// Fix the PHP code stuff...
1922
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1923
						$data[0] = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data[0]);
1924
1925
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1926
						if (!empty($context['browser']['is_opera']))
1927
							$data[0] .= '&nbsp;';
1928
					}
1929
				},
1930
				'block_level' => true,
1931
			),
1932
			array(
1933
				'tag' => 'color',
1934
				'type' => 'unparsed_equals',
1935
				'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]?)\))\]',
1936
				'before' => '<span style="color: $1;" class="bbc_color">',
1937
				'after' => '</span>',
1938
			),
1939
			array(
1940
				'tag' => 'email',
1941
				'type' => 'unparsed_content',
1942
				'content' => '<a href="mailto:$1" class="bbc_email">$1</a>',
1943
				// @todo Should this respect guest_hideContacts?
1944
				'validate' => function(&$tag, &$data, $disabled)
1945
				{
1946
					$data = strtr($data, array('<br>' => ''));
1947
				},
1948
			),
1949
			array(
1950
				'tag' => 'email',
1951
				'type' => 'unparsed_equals',
1952
				'before' => '<a href="mailto:$1" class="bbc_email">',
1953
				'after' => '</a>',
1954
				// @todo Should this respect guest_hideContacts?
1955
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1956
				'disabled_after' => ' ($1)',
1957
			),
1958
			// Legacy (and just a link even when not disabled)
1959
			array(
1960
				'tag' => 'flash',
1961
				'type' => 'unparsed_commas_content',
1962
				'test' => '\d+,\d+\]',
1963
				'content' => '<a href="$1" target="_blank" rel="noopener">$1</a>',
1964
				'validate' => function (&$tag, &$data, $disabled)
1965
				{
1966
					$data[0] = normalize_iri($data[0]);
1967
1968
					$scheme = parse_iri($data[0], PHP_URL_SCHEME);
1969
					if (empty($scheme))
1970
						$data[0] = '//' . ltrim($data[0], ':/');
1971
1972
					$ascii_url = iri_to_url($data[0]);
1973
					if ($ascii_url !== $data[0])
1974
						$tag['content'] = str_replace('href="$1"', 'href="' . $ascii_url . '"', $tag['content']);
1975
				},
1976
			),
1977
			array(
1978
				'tag' => 'float',
1979
				'type' => 'unparsed_equals',
1980
				'test' => '(left|right)(\s+max=\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)?\]',
1981
				'before' => '<div $1>',
1982
				'after' => '</div>',
1983
				'validate' => function(&$tag, &$data, $disabled)
1984
				{
1985
					$class = 'class="bbc_float float' . (strpos($data, 'left') === 0 ? 'left' : 'right') . '"';
1986
1987
					if (preg_match('~\bmax=(\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)~', $data, $matches))
1988
						$css = ' style="max-width:' . $matches[1] . (is_numeric($matches[1]) ? 'px' : '') . '"';
1989
					else
1990
						$css = '';
1991
1992
					$data = $class . $css;
1993
				},
1994
				'trim' => 'outside',
1995
				'block_level' => true,
1996
			),
1997
			// Legacy (alias of [url] with an FTP URL)
1998
			array(
1999
				'tag' => 'ftp',
2000
				'type' => 'unparsed_content',
2001
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
2002
				'validate' => function(&$tag, &$data, $disabled)
2003
				{
2004
					$data = normalize_iri(strtr($data, array('<br>' => '')));
2005
2006
					$scheme = parse_iri($data, PHP_URL_SCHEME);
2007
					if (empty($scheme))
2008
						$data = 'ftp://' . ltrim($data, ':/');
2009
2010
					$ascii_url = iri_to_url($data);
2011
					if ($ascii_url !== $data)
2012
						$tag['content'] = str_replace('href="$1"', 'href="' . $ascii_url . '"', $tag['content']);
2013
				},
2014
			),
2015
			// Legacy (alias of [url] with an FTP URL)
2016
			array(
2017
				'tag' => 'ftp',
2018
				'type' => 'unparsed_equals',
2019
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
2020
				'after' => '</a>',
2021
				'validate' => function(&$tag, &$data, $disabled)
2022
				{
2023
					$data = iri_to_url($data);
2024
2025
					$scheme = parse_iri($data, PHP_URL_SCHEME);
2026
					if (empty($scheme))
2027
						$data = 'ftp://' . ltrim($data, ':/');
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type boolean; however, parameter $string of ltrim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2027
						$data = 'ftp://' . ltrim(/** @scrutinizer ignore-type */ $data, ':/');
Loading history...
2028
				},
2029
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
2030
				'disabled_after' => ' ($1)',
2031
			),
2032
			array(
2033
				'tag' => 'font',
2034
				'type' => 'unparsed_equals',
2035
				'test' => '[A-Za-z0-9_,\-\s]+?\]',
2036
				'before' => '<span style="font-family: $1;" class="bbc_font">',
2037
				'after' => '</span>',
2038
			),
2039
			// Legacy (one of those things that should not be done)
2040
			array(
2041
				'tag' => 'glow',
2042
				'type' => 'unparsed_commas',
2043
				'test' => '[#0-9a-zA-Z\-]{3,12},([012]\d{1,2}|\d{1,2})(,[^]]+)?\]',
2044
				'before' => '<span style="text-shadow: $1 1px 1px 1px">',
2045
				'after' => '</span>',
2046
			),
2047
			// Legacy (alias of [color=green])
2048
			array(
2049
				'tag' => 'green',
2050
				'before' => '<span style="color: green;" class="bbc_color">',
2051
				'after' => '</span>',
2052
			),
2053
			array(
2054
				'tag' => 'html',
2055
				'type' => 'unparsed_content',
2056
				'content' => '<div>$1</div>',
2057
				'block_level' => true,
2058
				'disabled_content' => '$1',
2059
			),
2060
			array(
2061
				'tag' => 'hr',
2062
				'type' => 'closed',
2063
				'content' => '<hr>',
2064
				'block_level' => true,
2065
			),
2066
			array(
2067
				'tag' => 'i',
2068
				'before' => '<i>',
2069
				'after' => '</i>',
2070
			),
2071
			array(
2072
				'tag' => 'img',
2073
				'type' => 'unparsed_content',
2074
				'parameters' => array(
2075
					'alt' => array('optional' => true),
2076
					'title' => array('optional' => true),
2077
					'width' => array('optional' => true, 'value' => ' width="$1"', 'match' => '(\d+)'),
2078
					'height' => array('optional' => true, 'value' => ' height="$1"', 'match' => '(\d+)'),
2079
				),
2080
				'content' => '$1',
2081
				'validate' => function(&$tag, &$data, $disabled, $params)
2082
				{
2083
					$url = iri_to_url(strtr($data, array('<br>' => '')));
2084
2085
					if (parse_iri($url, PHP_URL_SCHEME) === null)
2086
						$url = '//' . ltrim($url, ':/');
2087
					else
2088
						$url = get_proxied_url($url);
2089
2090
					$alt = !empty($params['{alt}']) ? ' alt="' . $params['{alt}']. '"' : ' alt=""';
2091
					$title = !empty($params['{title}']) ? ' title="' . $params['{title}']. '"' : '';
2092
2093
					$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">';
2094
				},
2095
				'disabled_content' => '($1)',
2096
			),
2097
			array(
2098
				'tag' => 'iurl',
2099
				'type' => 'unparsed_content',
2100
				'content' => '<a href="$1" class="bbc_link">$1</a>',
2101
				'validate' => function(&$tag, &$data, $disabled)
2102
				{
2103
					$data = normalize_iri(strtr($data, array('<br>' => '')));
2104
2105
					$scheme = parse_iri($data, PHP_URL_SCHEME);
2106
					if (empty($scheme))
2107
						$data = '//' . ltrim($data, ':/');
2108
2109
					$ascii_url = iri_to_url($data);
2110
					if ($ascii_url !== $data)
2111
						$tag['content'] = str_replace('href="$1"', 'href="' . $ascii_url . '"', $tag['content']);
2112
				},
2113
			),
2114
			array(
2115
				'tag' => 'iurl',
2116
				'type' => 'unparsed_equals',
2117
				'quoted' => 'optional',
2118
				'before' => '<a href="$1" class="bbc_link">',
2119
				'after' => '</a>',
2120
				'validate' => function(&$tag, &$data, $disabled)
2121
				{
2122
					if (substr($data, 0, 1) == '#')
2123
						$data = '#post_' . substr($data, 1);
2124
					else
2125
					{
2126
						$data = iri_to_url($data);
2127
2128
						$scheme = parse_iri($data, PHP_URL_SCHEME);
2129
						if (empty($scheme))
2130
							$data = '//' . ltrim($data, ':/');
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type boolean; however, parameter $string of ltrim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2130
							$data = '//' . ltrim(/** @scrutinizer ignore-type */ $data, ':/');
Loading history...
2131
					}
2132
				},
2133
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
2134
				'disabled_after' => ' ($1)',
2135
			),
2136
			array(
2137
				'tag' => 'justify',
2138
				'before' => '<div class="justifytext">',
2139
				'after' => '</div>',
2140
				'block_level' => true,
2141
			),
2142
			array(
2143
				'tag' => 'left',
2144
				'before' => '<div class="lefttext">',
2145
				'after' => '</div>',
2146
				'block_level' => true,
2147
			),
2148
			array(
2149
				'tag' => 'li',
2150
				'before' => '<li>',
2151
				'after' => '</li>',
2152
				'trim' => 'outside',
2153
				'require_parents' => array('list'),
2154
				'block_level' => true,
2155
				'disabled_before' => '',
2156
				'disabled_after' => '<br>',
2157
			),
2158
			array(
2159
				'tag' => 'list',
2160
				'before' => '<ul class="bbc_list">',
2161
				'after' => '</ul>',
2162
				'trim' => 'inside',
2163
				'require_children' => array('li', 'list'),
2164
				'block_level' => true,
2165
			),
2166
			array(
2167
				'tag' => 'list',
2168
				'parameters' => array(
2169
					'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)'),
2170
				),
2171
				'before' => '<ul class="bbc_list" style="list-style-type: {type};">',
2172
				'after' => '</ul>',
2173
				'trim' => 'inside',
2174
				'require_children' => array('li'),
2175
				'block_level' => true,
2176
			),
2177
			array(
2178
				'tag' => 'ltr',
2179
				'before' => '<bdo dir="ltr">',
2180
				'after' => '</bdo>',
2181
				'block_level' => true,
2182
			),
2183
			array(
2184
				'tag' => 'me',
2185
				'type' => 'unparsed_equals',
2186
				'before' => '<div class="meaction">* $1 ',
2187
				'after' => '</div>',
2188
				'quoted' => 'optional',
2189
				'block_level' => true,
2190
				'disabled_before' => '/me ',
2191
				'disabled_after' => '<br>',
2192
			),
2193
			array(
2194
				'tag' => 'member',
2195
				'type' => 'unparsed_equals',
2196
				'before' => '<a href="' . $scripturl . '?action=profile;u=$1" class="mention" data-mention="$1">@',
2197
				'after' => '</a>',
2198
			),
2199
			// Legacy (horrible memories of the 1990s)
2200
			array(
2201
				'tag' => 'move',
2202
				'before' => '<marquee>',
2203
				'after' => '</marquee>',
2204
				'block_level' => true,
2205
				'disallow_children' => array('move'),
2206
			),
2207
			array(
2208
				'tag' => 'nobbc',
2209
				'type' => 'unparsed_content',
2210
				'content' => '$1',
2211
			),
2212
			array(
2213
				'tag' => 'php',
2214
				'type' => 'unparsed_content',
2215
				'content' => '<span class="phpcode">$1</span>',
2216
				'validate' => isset($disabled['php']) ? null : function(&$tag, &$data, $disabled)
2217
				{
2218
					if (!isset($disabled['php']))
2219
					{
2220
						$add_begin = substr(trim($data), 0, 5) != '&lt;?';
2221
						$data = highlight_php_code($add_begin ? '&lt;?php ' . $data . '?&gt;' : $data);
2222
						if ($add_begin)
2223
							$data = preg_replace(array('~^(.+?)&lt;\?.{0,40}?php(?:&nbsp;|\s)~', '~\?&gt;((?:</(font|span)>)*)$~'), '$1', $data, 2);
2224
					}
2225
				},
2226
				'block_level' => false,
2227
				'disabled_content' => '$1',
2228
			),
2229
			array(
2230
				'tag' => 'pre',
2231
				'before' => '<pre>',
2232
				'after' => '</pre>',
2233
			),
2234
			array(
2235
				'tag' => 'quote',
2236
				'before' => '<blockquote><cite>' . $txt['quote'] . '</cite>',
2237
				'after' => '</blockquote>',
2238
				'trim' => 'both',
2239
				'block_level' => true,
2240
			),
2241
			array(
2242
				'tag' => 'quote',
2243
				'parameters' => array(
2244
					'author' => array('match' => '(.{1,192}?)', 'quoted' => true),
2245
				),
2246
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
2247
				'after' => '</blockquote>',
2248
				'trim' => 'both',
2249
				'block_level' => true,
2250
			),
2251
			array(
2252
				'tag' => 'quote',
2253
				'type' => 'parsed_equals',
2254
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': $1</cite>',
2255
				'after' => '</blockquote>',
2256
				'trim' => 'both',
2257
				'quoted' => 'optional',
2258
				// Don't allow everything to be embedded with the author name.
2259
				'parsed_tags_allowed' => array('url', 'iurl', 'ftp'),
2260
				'block_level' => true,
2261
			),
2262
			array(
2263
				'tag' => 'quote',
2264
				'parameters' => array(
2265
					'author' => array('match' => '([^<>]{1,192}?)'),
2266
					'link' => array('match' => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|msg=\d+?|action=profile;u=\d+)'),
2267
					'date' => array('match' => '(\d+)', 'validate' => 'timeformat'),
2268
				),
2269
				'before' => '<blockquote><cite><a href="' . $scripturl . '?{link}">' . $txt['quote_from'] . ': {author} ' . $txt['search_on'] . ' {date}</a></cite>',
2270
				'after' => '</blockquote>',
2271
				'trim' => 'both',
2272
				'block_level' => true,
2273
			),
2274
			array(
2275
				'tag' => 'quote',
2276
				'parameters' => array(
2277
					'author' => array('match' => '(.{1,192}?)'),
2278
				),
2279
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
2280
				'after' => '</blockquote>',
2281
				'trim' => 'both',
2282
				'block_level' => true,
2283
			),
2284
			// Legacy (alias of [color=red])
2285
			array(
2286
				'tag' => 'red',
2287
				'before' => '<span style="color: red;" class="bbc_color">',
2288
				'after' => '</span>',
2289
			),
2290
			array(
2291
				'tag' => 'right',
2292
				'before' => '<div class="righttext">',
2293
				'after' => '</div>',
2294
				'block_level' => true,
2295
			),
2296
			array(
2297
				'tag' => 'rtl',
2298
				'before' => '<bdo dir="rtl">',
2299
				'after' => '</bdo>',
2300
				'block_level' => true,
2301
			),
2302
			array(
2303
				'tag' => 's',
2304
				'before' => '<s>',
2305
				'after' => '</s>',
2306
			),
2307
			// Legacy (never a good idea)
2308
			array(
2309
				'tag' => 'shadow',
2310
				'type' => 'unparsed_commas',
2311
				'test' => '[#0-9a-zA-Z\-]{3,12},(left|right|top|bottom|[0123]\d{0,2})\]',
2312
				'before' => '<span style="text-shadow: $1 $2">',
2313
				'after' => '</span>',
2314
				'validate' => function(&$tag, &$data, $disabled)
2315
				{
2316
2317
					if ($data[1] == 'top' || (is_numeric($data[1]) && $data[1] < 50))
2318
						$data[1] = '0 -2px 1px';
2319
2320
					elseif ($data[1] == 'right' || (is_numeric($data[1]) && $data[1] < 100))
2321
						$data[1] = '2px 0 1px';
2322
2323
					elseif ($data[1] == 'bottom' || (is_numeric($data[1]) && $data[1] < 190))
2324
						$data[1] = '0 2px 1px';
2325
2326
					elseif ($data[1] == 'left' || (is_numeric($data[1]) && $data[1] < 280))
2327
						$data[1] = '-2px 0 1px';
2328
2329
					else
2330
						$data[1] = '1px 1px 1px';
2331
				},
2332
			),
2333
			array(
2334
				'tag' => 'size',
2335
				'type' => 'unparsed_equals',
2336
				'test' => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)\]',
2337
				'before' => '<span style="font-size: $1;" class="bbc_size">',
2338
				'after' => '</span>',
2339
			),
2340
			array(
2341
				'tag' => 'size',
2342
				'type' => 'unparsed_equals',
2343
				'test' => '[1-7]\]',
2344
				'before' => '<span style="font-size: $1;" class="bbc_size">',
2345
				'after' => '</span>',
2346
				'validate' => function(&$tag, &$data, $disabled)
2347
				{
2348
					$sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95);
2349
					$data = $sizes[$data] . 'em';
2350
				},
2351
			),
2352
			array(
2353
				'tag' => 'sub',
2354
				'before' => '<sub>',
2355
				'after' => '</sub>',
2356
			),
2357
			array(
2358
				'tag' => 'sup',
2359
				'before' => '<sup>',
2360
				'after' => '</sup>',
2361
			),
2362
			array(
2363
				'tag' => 'table',
2364
				'before' => '<table class="bbc_table">',
2365
				'after' => '</table>',
2366
				'trim' => 'inside',
2367
				'require_children' => array('tr'),
2368
				'block_level' => true,
2369
			),
2370
			array(
2371
				'tag' => 'td',
2372
				'before' => '<td>',
2373
				'after' => '</td>',
2374
				'require_parents' => array('tr'),
2375
				'trim' => 'outside',
2376
				'block_level' => true,
2377
				'disabled_before' => '',
2378
				'disabled_after' => '',
2379
			),
2380
			array(
2381
				'tag' => 'time',
2382
				'type' => 'unparsed_content',
2383
				'content' => '$1',
2384
				'validate' => function(&$tag, &$data, $disabled)
2385
				{
2386
					if (is_numeric($data))
2387
						$data = timeformat($data);
2388
2389
					$tag['content'] = '<span class="bbc_time">$1</span>';
2390
				},
2391
			),
2392
			array(
2393
				'tag' => 'tr',
2394
				'before' => '<tr>',
2395
				'after' => '</tr>',
2396
				'require_parents' => array('table'),
2397
				'require_children' => array('td'),
2398
				'trim' => 'both',
2399
				'block_level' => true,
2400
				'disabled_before' => '',
2401
				'disabled_after' => '',
2402
			),
2403
			// Legacy (the <tt> element is dead)
2404
			array(
2405
				'tag' => 'tt',
2406
				'before' => '<span class="monospace">',
2407
				'after' => '</span>',
2408
			),
2409
			array(
2410
				'tag' => 'u',
2411
				'before' => '<u>',
2412
				'after' => '</u>',
2413
			),
2414
			array(
2415
				'tag' => 'url',
2416
				'type' => 'unparsed_content',
2417
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
2418
				'validate' => function(&$tag, &$data, $disabled)
2419
				{
2420
					$data = normalize_iri(strtr($data, array('<br>' => '')));
2421
2422
					$scheme = parse_iri($data, PHP_URL_SCHEME);
2423
					if (empty($scheme))
2424
						$data = '//' . ltrim($data, ':/');
2425
2426
					$ascii_url = iri_to_url($data);
2427
					if ($ascii_url !== $data)
2428
						$tag['content'] = str_replace('href="$1"', 'href="' . $ascii_url . '"', $tag['content']);
2429
				},
2430
			),
2431
			array(
2432
				'tag' => 'url',
2433
				'type' => 'unparsed_equals',
2434
				'quoted' => 'optional',
2435
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
2436
				'after' => '</a>',
2437
				'validate' => function(&$tag, &$data, $disabled)
2438
				{
2439
					$data = iri_to_url($data);
2440
2441
					$scheme = parse_iri($data, PHP_URL_SCHEME);
2442
					if (empty($scheme))
2443
						$data = '//' . ltrim($data, ':/');
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type boolean; however, parameter $string of ltrim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2443
						$data = '//' . ltrim(/** @scrutinizer ignore-type */ $data, ':/');
Loading history...
2444
				},
2445
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
2446
				'disabled_after' => ' ($1)',
2447
			),
2448
			// Legacy (alias of [color=white])
2449
			array(
2450
				'tag' => 'white',
2451
				'before' => '<span style="color: white;" class="bbc_color">',
2452
				'after' => '</span>',
2453
			),
2454
			array(
2455
				'tag' => 'youtube',
2456
				'type' => 'unparsed_content',
2457
				'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>',
2458
				'disabled_content' => '<a href="https://www.youtube.com/watch?v=$1" target="_blank" rel="noopener">https://www.youtube.com/watch?v=$1</a>',
2459
				'block_level' => true,
2460
			),
2461
		);
2462
2463
		// Inside these tags autolink is not recommendable.
2464
		$no_autolink_tags = array(
2465
			'url',
2466
			'iurl',
2467
			'email',
2468
			'img',
2469
			'html',
2470
		);
2471
2472
		// Let mods add new BBC without hassle.
2473
		call_integration_hook('integrate_bbc_codes', array(&$codes, &$no_autolink_tags));
2474
2475
		// This is mainly for the bbc manager, so it's easy to add tags above.  Custom BBC should be added above this line.
2476
		if ($message === false)
0 ignored issues
show
introduced by
The condition $message === false is always false.
Loading history...
2477
		{
2478
			usort(
2479
				$codes,
2480
				function($a, $b)
2481
				{
2482
					return strcmp($a['tag'], $b['tag']);
2483
				}
2484
			);
2485
			return $codes;
2486
		}
2487
2488
		// So the parser won't skip them.
2489
		$itemcodes = array(
2490
			'*' => 'disc',
2491
			'@' => 'disc',
2492
			'+' => 'square',
2493
			'x' => 'square',
2494
			'#' => 'square',
2495
			'o' => 'circle',
2496
			'O' => 'circle',
2497
			'0' => 'circle',
2498
		);
2499
		if (!isset($disabled['li']) && !isset($disabled['list']))
2500
		{
2501
			foreach ($itemcodes as $c => $dummy)
2502
				$bbc_codes[$c] = array();
2503
		}
2504
2505
		// Shhhh!
2506
		if (!isset($disabled['color']))
2507
		{
2508
			$codes[] = array(
2509
				'tag' => 'chrissy',
2510
				'before' => '<span style="color: #cc0099;">',
2511
				'after' => ' :-*</span>',
2512
			);
2513
			$codes[] = array(
2514
				'tag' => 'kissy',
2515
				'before' => '<span style="color: #cc0099;">',
2516
				'after' => ' :-*</span>',
2517
			);
2518
		}
2519
		$codes[] = array(
2520
			'tag' => 'cowsay',
2521
			'parameters' => array(
2522
				'e' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => 'oo', 'validate' => function ($eyes) use ($smcFunc)
2523
					{
2524
						return $smcFunc['substr']($eyes . 'oo', 0, 2);
2525
					},
2526
				),
2527
				't' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => '  ', 'validate' => function ($tongue) use ($smcFunc)
2528
					{
2529
						return $smcFunc['substr']($tongue . '  ', 0, 2);
2530
					},
2531
				),
2532
			),
2533
			'before' => '<pre data-e="{e}" data-t="{t}"><div>',
2534
			'after' => '</div></pre>',
2535
			'block_level' => true,
2536
			'validate' => function(&$tag, &$data, $disabled, $params)
2537
			{
2538
				static $moo = true;
2539
2540
				if ($moo)
2541
				{
2542
					addInlineJavaScript("\n\t" . base64_decode(
2543
						'aWYoZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImJvdmluZV9vcmFjbGU
2544
						iKT09PW51bGwpe2xldCBzdHlsZU5vZGU9ZG9jdW1lbnQuY3JlYXRlRWx
2545
						lbWVudCgic3R5bGUiKTtzdHlsZU5vZGUuaWQ9ImJvdmluZV9vcmFjbGU
2546
						iO3N0eWxlTm9kZS5pbm5lckhUTUw9J3ByZVtkYXRhLWVdW2RhdGEtdF1
2547
						7d2hpdGUtc3BhY2U6cHJlLXdyYXA7bGluZS1oZWlnaHQ6aW5pdGlhbDt
2548
						9cHJlW2RhdGEtZV1bZGF0YS10XSA+IGRpdntkaXNwbGF5OnRhYmxlO2J
2549
						vcmRlcjoxcHggc29saWQ7Ym9yZGVyLXJhZGl1czowLjVlbTtwYWRkaW5
2550
						nOjFjaDttYXgtd2lkdGg6ODBjaDttaW4td2lkdGg6MTJjaDt9cHJlW2R
2551
						hdGEtZV1bZGF0YS10XTo6YWZ0ZXJ7ZGlzcGxheTppbmxpbmUtYmxvY2s
2552
						7bWFyZ2luLWxlZnQ6OGNoO21pbi13aWR0aDoyMGNoO2RpcmVjdGlvbjp
2553
						sdHI7Y29udGVudDpcJ1xcNUMgXCdcJyBcJ1wnIF5fX15cXEEgXCdcJyB
2554
						cXDVDIFwnXCcgKFwnIGF0dHIoZGF0YS1lKSBcJylcXDVDX19fX19fX1x
2555
						cQSBcJ1wnIFwnXCcgXCdcJyAoX18pXFw1QyBcJ1wnIFwnXCcgXCdcJyB
2556
						cJ1wnIFwnXCcgXCdcJyBcJ1wnIClcXDVDL1xcNUNcXEEgXCdcJyBcJ1w
2557
						nIFwnXCcgXCdcJyBcJyBhdHRyKGRhdGEtdCkgXCcgfHwtLS0tdyB8XFx
2558
						BIFwnXCcgXCdcJyBcJ1wnIFwnXCcgXCdcJyBcJ1wnIFwnXCcgfHwgXCd
2559
						cJyBcJ1wnIFwnXCcgXCdcJyB8fFwnO30nO2RvY3VtZW50LmdldEVsZW1
2560
						lbnRzQnlUYWdOYW1lKCJoZWFkIilbMF0uYXBwZW5kQ2hpbGQoc3R5bGV
2561
						Ob2RlKTt9'
2562
					), true);
2563
2564
					$moo = false;
2565
				}
2566
			}
2567
		);
2568
2569
		foreach ($codes as $code)
2570
		{
2571
			// Make it easier to process parameters later
2572
			if (!empty($code['parameters']))
2573
				ksort($code['parameters'], SORT_STRING);
2574
2575
			// If we are not doing every tag only do ones we are interested in.
2576
			if (empty($parse_tags) || in_array($code['tag'], $parse_tags))
2577
				$bbc_codes[substr($code['tag'], 0, 1)][] = $code;
2578
		}
2579
		$codes = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $codes is dead and can be removed.
Loading history...
2580
	}
2581
2582
	// Shall we take the time to cache this?
2583
	if ($cache_id != '' && !empty($cache_enable) && (($cache_enable >= 2 && isset($message[1000])) || isset($message[2400])) && empty($parse_tags))
2584
	{
2585
		// It's likely this will change if the message is modified.
2586
		$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']);
2587
2588
		if (($temp = cache_get_data($cache_key, 240)) != null)
2589
			return $temp;
2590
2591
		$cache_t = microtime(true);
2592
	}
2593
2594
	if ($smileys === 'print')
0 ignored issues
show
introduced by
The condition $smileys === 'print' is always false.
Loading history...
2595
	{
2596
		// [glow], [shadow], and [move] can't really be printed.
2597
		$disabled['glow'] = true;
2598
		$disabled['shadow'] = true;
2599
		$disabled['move'] = true;
2600
2601
		// Colors can't well be displayed... supposed to be black and white.
2602
		$disabled['color'] = true;
2603
		$disabled['black'] = true;
2604
		$disabled['blue'] = true;
2605
		$disabled['white'] = true;
2606
		$disabled['red'] = true;
2607
		$disabled['green'] = true;
2608
		$disabled['me'] = true;
2609
2610
		// Color coding doesn't make sense.
2611
		$disabled['php'] = true;
2612
2613
		// Links are useless on paper... just show the link.
2614
		$disabled['ftp'] = true;
2615
		$disabled['url'] = true;
2616
		$disabled['iurl'] = true;
2617
		$disabled['email'] = true;
2618
		$disabled['flash'] = true;
2619
2620
		// @todo Change maybe?
2621
		if (!isset($_GET['images']))
2622
		{
2623
			$disabled['img'] = true;
2624
			$disabled['attach'] = true;
2625
		}
2626
2627
		// Maybe some custom BBC need to be disabled for printing.
2628
		call_integration_hook('integrate_bbc_print', array(&$disabled));
2629
	}
2630
2631
	$open_tags = array();
2632
	$message = strtr($message, array("\n" => '<br>'));
2633
2634
	if (!empty($parse_tags))
2635
	{
2636
		$real_alltags_regex = $alltags_regex;
2637
		$alltags_regex = '';
2638
	}
2639
	if (empty($alltags_regex))
2640
	{
2641
		$alltags = array();
2642
		foreach ($bbc_codes as $section)
2643
		{
2644
			foreach ($section as $code)
2645
				$alltags[] = $code['tag'];
2646
		}
2647
		$alltags_regex = '(?' . '>\b' . build_regex(array_unique($alltags)) . '\b|' . build_regex(array_keys($itemcodes)) . ')';
0 ignored issues
show
Bug introduced by
Are you sure build_regex(array_unique($alltags)) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

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

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

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

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

2881
							$pcre_subroutines['bracket_quote'] = '[' . $bracket_quote_chars . ']|&' . /** @scrutinizer ignore-type */ build_regex($bracket_quote_entities, '~');
Loading history...
2882
							$pcre_subroutines['allowed_entities'] = '&(?!' . build_regex(array_merge($bracket_quote_entities, array('lt;', 'gt;')), '~') . ')';
0 ignored issues
show
Bug introduced by
Are you sure build_regex(array_merge(...ay('lt;', 'gt;')), '~') of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

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

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

3091
								$url_regex .= '(?<' . $name . '>' . /** @scrutinizer ignore-type */ $subroutine . ')';
Loading history...
3092
3093
							$url_regex .= ')';
3094
						}
3095
3096
						$tmp_data = preg_replace_callback(
3097
							'~' . $url_regex . '~i' . ($context['utf8'] ? 'u' : ''),
3098
							function($matches) use ($schemes)
3099
							{
3100
								$url = array_shift($matches);
3101
3102
								// If this isn't a clean URL, bail out
3103
								if ($url != sanitize_iri($url))
3104
									return $url;
3105
3106
								// Ensure the host name is in its canonical form.
3107
								$url = normalize_iri($url);
3108
3109
								$parsedurl = parse_iri($url);
3110
3111
								if (!isset($parsedurl['scheme']))
3112
									$parsedurl['scheme'] = '';
3113
3114
								if ($parsedurl['scheme'] == 'mailto')
3115
								{
3116
									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...
3117
										return $url;
3118
3119
									// Is this version of PHP capable of validating this email address?
3120
									$can_validate = defined('FILTER_FLAG_EMAIL_UNICODE') || strlen($parsedurl['path']) == strspn(strtolower($parsedurl['path']), 'abcdefghijklmnopqrstuvwxyz0123456789!#$%&\'*+-/=?^_`{|}~.@');
3121
3122
									$flags = defined('FILTER_FLAG_EMAIL_UNICODE') ? FILTER_FLAG_EMAIL_UNICODE : null;
3123
3124
									if (!$can_validate || filter_var($parsedurl['path'], FILTER_VALIDATE_EMAIL, $flags) !== false)
0 ignored issues
show
Bug introduced by
It seems like $flags can also be of type null; however, parameter $options of filter_var() does only seem to accept array|integer, maybe add an additional type check? ( Ignorable by Annotation )

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

3124
									if (!$can_validate || filter_var($parsedurl['path'], FILTER_VALIDATE_EMAIL, /** @scrutinizer ignore-type */ $flags) !== false)
Loading history...
3125
										return '[email=' . str_replace('mailto:', '', $url) . ']' . $url . '[/email]';
3126
									else
3127
										return $url;
3128
								}
3129
3130
								// Are we linking a schemeless URL or naked domain name (e.g. "example.com")?
3131
								if (empty($parsedurl['scheme']))
3132
									$fullUrl = '//' . ltrim($url, ':/');
3133
								else
3134
									$fullUrl = $url;
3135
3136
								// Make sure that $fullUrl really is valid
3137
								if (in_array($parsedurl['scheme'], $schemes['forbidden']) || (!in_array($parsedurl['scheme'], $schemes['no_authority']) && validate_iri((strpos($fullUrl, '//') === 0 ? 'http:' : '') . $fullUrl) === false))
3138
									return $url;
3139
3140
								return '[url=&quot;' . str_replace(array('[', ']'), array('&#91;', '&#93;'), iri_to_url($fullUrl)) . '&quot;]' . $url . '[/url]';
3141
							},
3142
							$data
3143
						);
3144
3145
						if (!is_null($tmp_data))
3146
							$data = $tmp_data;
3147
					}
3148
3149
					// Next, emails...  Must be careful not to step on enablePostHTML logic above...
3150
					if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false && stripos($data, 'mailto:') === false)
3151
					{
3152
						// Preceded by a space or start of line
3153
						$email_regex = '(?<=^|<br>|[\h\v])' .
3154
3155
						// An email address
3156
						'[' . $domain_label_chars . '_.]{1,80}' .
3157
						'@' .
3158
						'[' . $domain_label_chars . '.]+' .
3159
						'\.' . $modSettings['tld_regex'] .
3160
3161
						// Followed by a non-domain character or end of line
3162
						'(?=[^' . $domain_label_chars . ']|$)';
3163
3164
						$tmp_data = preg_replace('~' . $email_regex . '~i' . ($context['utf8'] ? 'u' : ''), '[email]$0[/email]', $data);
3165
3166
						if (!is_null($tmp_data))
3167
							$data = $tmp_data;
3168
					}
3169
3170
					// Save a little memory.
3171
					unset($tmp_data);
3172
				}
3173
			}
3174
3175
			// Restore any placeholders
3176
			$data = strtr($data, $placeholders);
3177
3178
			$data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));
3179
3180
			// If it wasn't changed, no copying or other boring stuff has to happen!
3181
			if ($data != substr($message, $last_pos, $pos - $last_pos))
3182
			{
3183
				$message = substr($message, 0, $last_pos) . $data . substr($message, $pos);
3184
3185
				// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
3186
				$old_pos = strlen($data) + $last_pos;
3187
				$pos = strpos($message, '[', $last_pos);
3188
				$pos = $pos === false ? $old_pos : min($pos, $old_pos);
3189
			}
3190
		}
3191
3192
		// Are we there yet?  Are we there yet?
3193
		if ($pos >= strlen($message) - 1)
3194
			break;
3195
3196
		$tag_character = strtolower($message[$pos + 1]);
3197
3198
		if ($tag_character == '/' && !empty($open_tags))
3199
		{
3200
			$pos2 = strpos($message, ']', $pos + 1);
3201
			if ($pos2 == $pos + 2)
3202
				continue;
3203
3204
			$look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));
3205
3206
			// A closing tag that doesn't match any open tags? Skip it.
3207
			if (!in_array($look_for, array_map(function($code) { return $code['tag']; }, $open_tags)))
3208
				continue;
3209
3210
			$to_close = array();
3211
			$block_level = null;
3212
3213
			do
3214
			{
3215
				$tag = array_pop($open_tags);
3216
				if (!$tag)
3217
					break;
3218
3219
				if (!empty($tag['block_level']))
3220
				{
3221
					// Only find out if we need to.
3222
					if ($block_level === false)
3223
					{
3224
						array_push($open_tags, $tag);
3225
						break;
3226
					}
3227
3228
					// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
3229
					if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]]))
3230
					{
3231
						foreach ($bbc_codes[$look_for[0]] as $temp)
3232
							if ($temp['tag'] == $look_for)
3233
							{
3234
								$block_level = !empty($temp['block_level']);
3235
								break;
3236
							}
3237
					}
3238
3239
					if ($block_level !== true)
3240
					{
3241
						$block_level = false;
3242
						array_push($open_tags, $tag);
3243
						break;
3244
					}
3245
				}
3246
3247
				$to_close[] = $tag;
3248
			}
3249
			while ($tag['tag'] != $look_for);
3250
3251
			// Did we just eat through everything and not find it?
3252
			if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
3253
			{
3254
				$open_tags = $to_close;
3255
				continue;
3256
			}
3257
			elseif (!empty($to_close) && $tag['tag'] != $look_for)
3258
			{
3259
				if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]]))
3260
				{
3261
					foreach ($bbc_codes[$look_for[0]] as $temp)
3262
						if ($temp['tag'] == $look_for)
3263
						{
3264
							$block_level = !empty($temp['block_level']);
3265
							break;
3266
						}
3267
				}
3268
3269
				// We're not looking for a block level tag (or maybe even a tag that exists...)
3270
				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...
3271
				{
3272
					foreach ($to_close as $tag)
3273
						array_push($open_tags, $tag);
3274
					continue;
3275
				}
3276
			}
3277
3278
			foreach ($to_close as $tag)
3279
			{
3280
				$message = substr($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1);
3281
				$pos += strlen($tag['after']) + 2;
3282
				$pos2 = $pos - 1;
3283
3284
				// See the comment at the end of the big loop - just eating whitespace ;).
3285
				$whitespace_regex = '';
3286
				if (!empty($tag['block_level']))
3287
					$whitespace_regex .= '(&nbsp;|\s)*(<br\s*/?' . '>)?';
3288
				// Trim one line of whitespace after unnested tags, but all of it after nested ones
3289
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
3290
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
3291
3292
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
3293
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
3294
			}
3295
3296
			if (!empty($to_close))
3297
			{
3298
				$to_close = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $to_close is dead and can be removed.
Loading history...
3299
				$pos--;
3300
			}
3301
3302
			continue;
3303
		}
3304
3305
		// No tags for this character, so just keep going (fastest possible course.)
3306
		if (!isset($bbc_codes[$tag_character]))
3307
			continue;
3308
3309
		$inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
3310
		$tag = null;
3311
		foreach ($bbc_codes[$tag_character] as $possible)
3312
		{
3313
			$pt_strlen = strlen($possible['tag']);
3314
3315
			// Not a match?
3316
			if (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag'])
3317
				continue;
3318
3319
			$next_c = isset($message[$pos + 1 + $pt_strlen]) ? $message[$pos + 1 + $pt_strlen] : '';
3320
3321
			// A tag is the last char maybe
3322
			if ($next_c == '')
3323
				break;
3324
3325
			// A test validation?
3326
			if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0)
3327
				continue;
3328
			// Do we want parameters?
3329
			elseif (!empty($possible['parameters']))
3330
			{
3331
				// Are all the parameters optional?
3332
				$param_required = false;
3333
				foreach ($possible['parameters'] as $param)
3334
				{
3335
					if (empty($param['optional']))
3336
					{
3337
						$param_required = true;
3338
						break;
3339
					}
3340
				}
3341
3342
				if ($param_required && $next_c != ' ')
3343
					continue;
3344
			}
3345
			elseif (isset($possible['type']))
3346
			{
3347
				// Do we need an equal sign?
3348
				if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=')
3349
					continue;
3350
				// Maybe we just want a /...
3351
				if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + $pt_strlen, 2) != '/]' && substr($message, $pos + 1 + $pt_strlen, 3) != ' /]')
3352
					continue;
3353
				// An immediate ]?
3354
				if ($possible['type'] == 'unparsed_content' && $next_c != ']')
3355
					continue;
3356
			}
3357
			// No type means 'parsed_content', which demands an immediate ] without parameters!
3358
			elseif ($next_c != ']')
3359
				continue;
3360
3361
			// Check allowed tree?
3362
			if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
3363
				continue;
3364
			elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
3365
				continue;
3366
			// If this is in the list of disallowed child tags, don't parse it.
3367
			elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
3368
				continue;
3369
3370
			$pos1 = $pos + 1 + $pt_strlen + 1;
3371
3372
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
3373
			if ($possible['tag'] == 'quote')
3374
			{
3375
				// Start with standard
3376
				$quote_alt = false;
3377
				foreach ($open_tags as $open_quote)
3378
				{
3379
					// Every parent quote this quote has flips the styling
3380
					if ($open_quote['tag'] == 'quote')
3381
						$quote_alt = !$quote_alt;
0 ignored issues
show
introduced by
The condition $quote_alt is always false.
Loading history...
3382
				}
3383
				// Add a class to the quote to style alternating blockquotes
3384
				$possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">'));
3385
			}
3386
3387
			// This is long, but it makes things much easier and cleaner.
3388
			if (!empty($possible['parameters']))
3389
			{
3390
				// Build a regular expression for each parameter for the current tag.
3391
				$regex_key = $smcFunc['json_encode']($possible['parameters']);
3392
				if (!isset($params_regexes[$regex_key]))
3393
				{
3394
					$params_regexes[$regex_key] = '';
3395
3396
					foreach ($possible['parameters'] as $p => $info)
3397
						$params_regexes[$regex_key] .= '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . '\s*)' . (empty($info['optional']) ? '' : '?');
3398
				}
3399
3400
				// Extract the string that potentially holds our parameters.
3401
				$blob = preg_split('~\[/?(?:' . $alltags_regex . ')~i', substr($message, $pos));
3402
				$blobs = preg_split('~\]~i', $blob[1]);
3403
3404
				$splitters = implode('=|', array_keys($possible['parameters'])) . '=';
3405
3406
				// Progressively append more blobs until we find our parameters or run out of blobs
3407
				$blob_counter = 1;
3408
				while ($blob_counter <= count($blobs))
3409
				{
3410
					$given_param_string = implode(']', array_slice($blobs, 0, $blob_counter++));
3411
3412
					$given_params = preg_split('~\s(?=(' . $splitters . '))~i', $given_param_string);
3413
					sort($given_params, SORT_STRING);
3414
3415
					$match = preg_match('~^' . $params_regexes[$regex_key] . '$~i', implode(' ', $given_params), $matches) !== 0;
3416
3417
					if ($match)
3418
						break;
3419
				}
3420
3421
				// Didn't match our parameter list, try the next possible.
3422
				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...
3423
					continue;
3424
3425
				$params = array();
3426
				for ($i = 1, $n = count($matches); $i < $n; $i += 2)
3427
				{
3428
					$key = strtok(ltrim($matches[$i]), '=');
3429
					if ($key === false)
3430
						continue;
3431
					elseif (isset($possible['parameters'][$key]['value']))
3432
						$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
3433
					elseif (isset($possible['parameters'][$key]['validate']))
3434
						$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
3435
					else
3436
						$params['{' . $key . '}'] = $matches[$i + 1];
3437
3438
					// Just to make sure: replace any $ or { so they can't interpolate wrongly.
3439
					$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
3440
				}
3441
3442
				foreach ($possible['parameters'] as $p => $info)
3443
				{
3444
					if (!isset($params['{' . $p . '}']))
3445
					{
3446
						if (!isset($info['default']))
3447
							$params['{' . $p . '}'] = '';
3448
						elseif (isset($possible['parameters'][$p]['value']))
3449
							$params['{' . $p . '}'] = strtr($possible['parameters'][$p]['value'], array('$1' => $info['default']));
3450
						elseif (isset($possible['parameters'][$p]['validate']))
3451
							$params['{' . $p . '}'] = $possible['parameters'][$p]['validate']($info['default']);
3452
						else
3453
							$params['{' . $p . '}'] = $info['default'];
3454
					}
3455
				}
3456
3457
				$tag = $possible;
3458
3459
				// Put the parameters into the string.
3460
				if (isset($tag['before']))
3461
					$tag['before'] = strtr($tag['before'], $params);
3462
				if (isset($tag['after']))
3463
					$tag['after'] = strtr($tag['after'], $params);
3464
				if (isset($tag['content']))
3465
					$tag['content'] = strtr($tag['content'], $params);
3466
3467
				$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...
3468
			}
3469
			else
3470
			{
3471
				$tag = $possible;
3472
				$params = array();
3473
			}
3474
			break;
3475
		}
3476
3477
		// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
3478
		if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
3479
		{
3480
			if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
3481
				continue;
3482
3483
			$tag = $itemcodes[$message[$pos + 1]];
3484
3485
			// First let's set up the tree: it needs to be in a list, or after an li.
3486
			if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
3487
			{
3488
				$open_tags[] = array(
3489
					'tag' => 'list',
3490
					'after' => '</ul>',
3491
					'block_level' => true,
3492
					'require_children' => array('li'),
3493
					'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
3494
				);
3495
				$code = '<ul class="bbc_list">';
3496
			}
3497
			// We're in a list item already: another itemcode?  Close it first.
3498
			elseif ($inside['tag'] == 'li')
3499
			{
3500
				array_pop($open_tags);
3501
				$code = '</li>';
3502
			}
3503
			else
3504
				$code = '';
3505
3506
			// Now we open a new tag.
3507
			$open_tags[] = array(
3508
				'tag' => 'li',
3509
				'after' => '</li>',
3510
				'trim' => 'outside',
3511
				'block_level' => true,
3512
				'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
3513
			);
3514
3515
			// First, open the tag...
3516
			$code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
3517
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
3518
			$pos += strlen($code) - 1 + 2;
3519
3520
			// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
3521
			$pos2 = strpos($message, '<br>', $pos);
3522
			$pos3 = strpos($message, '[/', $pos);
3523
			if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
3524
			{
3525
				preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 4), $matches);
3526
				$message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
3527
3528
				$open_tags[count($open_tags) - 2]['after'] = '</ul>';
3529
			}
3530
			// Tell the [list] that it needs to close specially.
3531
			else
3532
			{
3533
				// Move the li over, because we're not sure what we'll hit.
3534
				$open_tags[count($open_tags) - 1]['after'] = '';
3535
				$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
3536
			}
3537
3538
			continue;
3539
		}
3540
3541
		// Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
3542
		if ($tag === null && $inside !== null && !empty($inside['require_children']))
3543
		{
3544
			array_pop($open_tags);
3545
3546
			$message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
3547
			$pos += strlen($inside['after']) - 1 + 2;
3548
		}
3549
3550
		// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
3551
		if ($tag === null)
3552
			continue;
3553
3554
		// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
3555
		if (isset($inside['disallow_children']))
3556
			$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
3557
3558
		// Is this tag disabled?
3559
		if (isset($disabled[$tag['tag']]))
3560
		{
3561
			if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
3562
			{
3563
				$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
3564
				$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
3565
				$tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
3566
			}
3567
			elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
3568
			{
3569
				$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
3570
				$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
3571
			}
3572
			else
3573
				$tag['content'] = $tag['disabled_content'];
3574
		}
3575
3576
		// we use this a lot
3577
		$tag_strlen = strlen($tag['tag']);
3578
3579
		// The only special case is 'html', which doesn't need to close things.
3580
		if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
3581
		{
3582
			$n = count($open_tags) - 1;
3583
			while (empty($open_tags[$n]['block_level']) && $n >= 0)
3584
				$n--;
3585
3586
			// Close all the non block level tags so this tag isn't surrounded by them.
3587
			for ($i = count($open_tags) - 1; $i > $n; $i--)
3588
			{
3589
				$message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
3590
				$ot_strlen = strlen($open_tags[$i]['after']);
3591
				$pos += $ot_strlen + 2;
3592
				$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...
3593
3594
				// Trim or eat trailing stuff... see comment at the end of the big loop.
3595
				$whitespace_regex = '';
3596
				if (!empty($tag['block_level']))
3597
					$whitespace_regex .= '(&nbsp;|\s)*(<br>)?';
3598
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
3599
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
3600
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
3601
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
3602
3603
				array_pop($open_tags);
3604
			}
3605
		}
3606
3607
		// Can't read past the end of the message
3608
		$pos1 = min(strlen($message), $pos1);
3609
3610
		// No type means 'parsed_content'.
3611
		if (!isset($tag['type']))
3612
		{
3613
			$open_tags[] = $tag;
3614
3615
			// There's no data to change, but maybe do something based on params?
3616
			$data = null;
3617
			if (isset($tag['validate']))
3618
				$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...
3619
3620
			$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
3621
			$pos += strlen($tag['before']) - 1 + 2;
3622
		}
3623
		// Don't parse the content, just skip it.
3624
		elseif ($tag['type'] == 'unparsed_content')
3625
		{
3626
			$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
3627
			if ($pos2 === false)
3628
				continue;
3629
3630
			$data = substr($message, $pos1, $pos2 - $pos1);
3631
3632
			if (!empty($tag['block_level']) && substr($data, 0, 4) == '<br>')
3633
				$data = substr($data, 4);
3634
3635
			if (isset($tag['validate']))
3636
				$tag['validate']($tag, $data, $disabled, $params);
3637
3638
			$code = strtr($tag['content'], array('$1' => $data));
3639
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
3640
3641
			$pos += strlen($code) - 1 + 2;
3642
			$last_pos = $pos + 1;
3643
		}
3644
		// Don't parse the content, just skip it.
3645
		elseif ($tag['type'] == 'unparsed_equals_content')
3646
		{
3647
			// The value may be quoted for some tags - check.
3648
			if (isset($tag['quoted']))
3649
			{
3650
				$quoted = substr($message, $pos1, 6) == '&quot;';
3651
				if ($tag['quoted'] != 'optional' && !$quoted)
3652
					continue;
3653
3654
				if ($quoted)
3655
					$pos1 += 6;
3656
			}
3657
			else
3658
				$quoted = false;
3659
3660
			$pos2 = strpos($message, $quoted == false ? ']' : '&quot;]', $pos1);
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

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

Loading history...
3661
			if ($pos2 === false)
3662
				continue;
3663
3664
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3665
			if ($pos3 === false)
3666
				continue;
3667
3668
			$data = array(
3669
				substr($message, $pos2 + ($quoted == false ? 1 : 7), $pos3 - ($pos2 + ($quoted == false ? 1 : 7))),
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

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

Loading history...
3670
				substr($message, $pos1, $pos2 - $pos1)
3671
			);
3672
3673
			if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '<br>')
3674
				$data[0] = substr($data[0], 4);
3675
3676
			// Validation for my parking, please!
3677
			if (isset($tag['validate']))
3678
				$tag['validate']($tag, $data, $disabled, $params);
3679
3680
			$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
3681
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3682
			$pos += strlen($code) - 1 + 2;
3683
		}
3684
		// A closed tag, with no content or value.
3685
		elseif ($tag['type'] == 'closed')
3686
		{
3687
			$pos2 = strpos($message, ']', $pos);
3688
3689
			// Maybe a custom BBC wants to do something special?
3690
			$data = null;
3691
			if (isset($tag['validate']))
3692
				$tag['validate']($tag, $data, $disabled, $params);
3693
3694
			$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
3695
			$pos += strlen($tag['content']) - 1 + 2;
3696
		}
3697
		// This one is sorta ugly... :/.  Unfortunately, it's needed for flash.
3698
		elseif ($tag['type'] == 'unparsed_commas_content')
3699
		{
3700
			$pos2 = strpos($message, ']', $pos1);
3701
			if ($pos2 === false)
3702
				continue;
3703
3704
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3705
			if ($pos3 === false)
3706
				continue;
3707
3708
			// We want $1 to be the content, and the rest to be csv.
3709
			$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
3710
			$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
3711
3712
			if (isset($tag['validate']))
3713
				$tag['validate']($tag, $data, $disabled, $params);
3714
3715
			$code = $tag['content'];
3716
			foreach ($data as $k => $d)
3717
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3718
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3719
			$pos += strlen($code) - 1 + 2;
3720
		}
3721
		// This has parsed content, and a csv value which is unparsed.
3722
		elseif ($tag['type'] == 'unparsed_commas')
3723
		{
3724
			$pos2 = strpos($message, ']', $pos1);
3725
			if ($pos2 === false)
3726
				continue;
3727
3728
			$data = explode(',', substr($message, $pos1, $pos2 - $pos1));
3729
3730
			if (isset($tag['validate']))
3731
				$tag['validate']($tag, $data, $disabled, $params);
3732
3733
			// Fix after, for disabled code mainly.
3734
			foreach ($data as $k => $d)
3735
				$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
3736
3737
			$open_tags[] = $tag;
3738
3739
			// Replace them out, $1, $2, $3, $4, etc.
3740
			$code = $tag['before'];
3741
			foreach ($data as $k => $d)
3742
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3743
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1);
3744
			$pos += strlen($code) - 1 + 2;
3745
		}
3746
		// A tag set to a value, parsed or not.
3747
		elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
3748
		{
3749
			// The value may be quoted for some tags - check.
3750
			if (isset($tag['quoted']))
3751
			{
3752
				$quoted = substr($message, $pos1, 6) == '&quot;';
3753
				if ($tag['quoted'] != 'optional' && !$quoted)
3754
					continue;
3755
3756
				if ($quoted)
3757
					$pos1 += 6;
3758
			}
3759
			else
3760
				$quoted = false;
3761
3762
			if ($quoted)
3763
			{
3764
				$end_of_value = strpos($message, '&quot;]', $pos1);
3765
				$nested_tag = strpos($message, '=&quot;', $pos1);
3766
				// Check so this is not just an quoted url ending with a =
3767
				if ($nested_tag && substr($message, $nested_tag, 8) == '=&quot;]')
3768
					$nested_tag = false;
3769
				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...
3770
					// Nested tag with quoted value detected, use next end tag
3771
					$nested_tag_pos = strpos($message, $quoted == false ? ']' : '&quot;]', $pos1) + 6;
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

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

Loading history...
3772
			}
3773
3774
			$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...
3775
			if ($pos2 === false)
3776
				continue;
3777
3778
			$data = substr($message, $pos1, $pos2 - $pos1);
3779
3780
			// Validation for my parking, please!
3781
			if (isset($tag['validate']))
3782
				$tag['validate']($tag, $data, $disabled, $params);
3783
3784
			// For parsed content, we must recurse to avoid security problems.
3785
			if ($tag['type'] != 'unparsed_equals')
3786
				$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
3787
3788
			$tag['after'] = strtr($tag['after'], array('$1' => $data));
3789
3790
			$open_tags[] = $tag;
3791
3792
			$code = strtr($tag['before'], array('$1' => $data));
3793
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + ($quoted == false ? 1 : 7));
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

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

Loading history...
3794
			$pos += strlen($code) - 1 + 2;
3795
		}
3796
3797
		// If this is block level, eat any breaks after it.
3798
		if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '<br>')
3799
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 5);
3800
3801
		// Are we trimming outside this tag?
3802
		if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
3803
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
3804
	}
3805
3806
	// Close any remaining tags.
3807
	while ($tag = array_pop($open_tags))
3808
		$message .= "\n" . $tag['after'] . "\n";
3809
3810
	// Parse the smileys within the parts where it can be done safely.
3811
	if ($smileys === true)
3812
	{
3813
		$message_parts = explode("\n", $message);
3814
		for ($i = 0, $n = count($message_parts); $i < $n; $i += 2)
3815
			parsesmileys($message_parts[$i]);
3816
3817
		$message = implode('', $message_parts);
3818
	}
3819
3820
	// No smileys, just get rid of the markers.
3821
	else
3822
		$message = strtr($message, array("\n" => ''));
3823
3824
	if ($message !== '' && $message[0] === ' ')
3825
		$message = '&nbsp;' . substr($message, 1);
3826
3827
	// Cleanup whitespace.
3828
	$message = strtr($message, array('  ' => ' &nbsp;', "\r" => '', "\n" => '<br>', '<br> ' => '<br>&nbsp;', '&#13;' => "\n"));
3829
3830
	// Allow mods access to what parse_bbc created
3831
	call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
3832
3833
	// Cache the output if it took some time...
3834
	if (isset($cache_key, $cache_t) && microtime(true) - $cache_t > 0.05)
3835
		cache_put_data($cache_key, $message, 240);
3836
3837
	// If this was a force parse revert if needed.
3838
	if (!empty($parse_tags))
3839
	{
3840
		$alltags_regex = empty($real_alltags_regex) ? '' : $real_alltags_regex;
3841
		unset($real_alltags_regex);
3842
	}
3843
	elseif (!empty($bbc_codes))
3844
		$bbc_lang_locales[$txt['lang_locale']] = $bbc_codes;
3845
3846
	return $message;
3847
}
3848
3849
/**
3850
 * Parse smileys in the passed message.
3851
 *
3852
 * The smiley parsing function which makes pretty faces appear :).
3853
 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
3854
 * These are specifically not parsed in code tags [url=mailto:[email protected]]
3855
 * Caches the smileys from the database or array in memory.
3856
 * Doesn't return anything, but rather modifies message directly.
3857
 *
3858
 * @param string &$message The message to parse smileys in
3859
 */
3860
function parsesmileys(&$message)
3861
{
3862
	global $modSettings, $txt, $user_info, $context, $smcFunc;
3863
	static $smileyPregSearch = null, $smileyPregReplacements = array();
3864
3865
	// No smiley set at all?!
3866
	if ($user_info['smiley_set'] == 'none' || trim($message) == '')
3867
		return;
3868
3869
	// Maybe a mod wants to implement an alternative method (e.g. emojis instead of images)
3870
	call_integration_hook('integrate_smileys', array(&$smileyPregSearch, &$smileyPregReplacements));
3871
3872
	// If smileyPregSearch hasn't been set, do it now.
3873
	if (empty($smileyPregSearch))
3874
	{
3875
		// Cache for longer when customized smiley codes aren't enabled
3876
		$cache_time = empty($modSettings['smiley_enable']) ? 7200 : 480;
3877
3878
		// Load the smileys in reverse order by length so they don't get parsed incorrectly.
3879
		if (($temp = cache_get_data('parsing_smileys_' . $user_info['smiley_set'], $cache_time)) == null)
3880
		{
3881
			$result = $smcFunc['db_query']('', '
3882
				SELECT s.code, f.filename, s.description
3883
				FROM {db_prefix}smileys AS s
3884
					JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
3885
				WHERE f.smiley_set = {string:smiley_set}' . (empty($modSettings['smiley_enable']) ? '
3886
					AND s.code IN ({array_string:default_codes})' : '') . '
3887
				ORDER BY LENGTH(s.code) DESC',
3888
				array(
3889
					'default_codes' => array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'),
3890
					'smiley_set' => $user_info['smiley_set'],
3891
				)
3892
			);
3893
			$smileysfrom = array();
3894
			$smileysto = array();
3895
			$smileysdescs = array();
3896
			while ($row = $smcFunc['db_fetch_assoc']($result))
3897
			{
3898
				$smileysfrom[] = $row['code'];
3899
				$smileysto[] = $smcFunc['htmlspecialchars']($row['filename']);
3900
				$smileysdescs[] = !empty($txt['icon_' . strtolower($row['description'])]) ? $txt['icon_' . strtolower($row['description'])] : $row['description'];
3901
			}
3902
			$smcFunc['db_free_result']($result);
3903
3904
			cache_put_data('parsing_smileys_' . $user_info['smiley_set'], array($smileysfrom, $smileysto, $smileysdescs), $cache_time);
3905
		}
3906
		else
3907
			list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
3908
3909
		// The non-breaking-space is a complex thing...
3910
		$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
3911
3912
		// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:[email protected]] doesn't parse the :D smiley)
3913
		$smileyPregReplacements = array();
3914
		$searchParts = array();
3915
		$smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
3916
3917
		for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
3918
		{
3919
			$specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES);
3920
			$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">';
3921
3922
			$smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
3923
3924
			$searchParts[] = $smileysfrom[$i];
3925
			if ($smileysfrom[$i] != $specialChars)
3926
			{
3927
				$smileyPregReplacements[$specialChars] = $smileyCode;
3928
				$searchParts[] = $specialChars;
3929
3930
				// Some 2.0 hex htmlchars are in there as 3 digits; allow for finding leading 0 or not
3931
				$specialChars2 = preg_replace('/&#(\d{2});/', '&#0$1;', $specialChars);
3932
				if ($specialChars2 != $specialChars)
3933
				{
3934
					$smileyPregReplacements[$specialChars2] = $smileyCode;
3935
					$searchParts[] = $specialChars2;
3936
				}
3937
			}
3938
		}
3939
3940
		$smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?<![a-zA-Z0-9])\(|^)(' . build_regex($searchParts, '~') . ')(?=[^[:alpha:]0-9]|$)~' . ($context['utf8'] ? 'u' : '');
0 ignored issues
show
Bug introduced by
Are you sure build_regex($searchParts, '~') of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

3940
		$smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?<![a-zA-Z0-9])\(|^)(' . /** @scrutinizer ignore-type */ build_regex($searchParts, '~') . ')(?=[^[:alpha:]0-9]|$)~' . ($context['utf8'] ? 'u' : '');
Loading history...
3941
	}
3942
3943
	// If there are no smileys defined, no need to replace anything
3944
	if (empty($smileyPregReplacements))
3945
		return;
3946
3947
	// Replace away!
3948
	$message = preg_replace_callback(
3949
		$smileyPregSearch,
3950
		function($matches) use ($smileyPregReplacements)
3951
		{
3952
			return $smileyPregReplacements[$matches[1]];
3953
		},
3954
		$message
3955
	);
3956
}
3957
3958
/**
3959
 * Highlight any code.
3960
 *
3961
 * Uses PHP's highlight_string() to highlight PHP syntax
3962
 * does special handling to keep the tabs in the code available.
3963
 * used to parse PHP code from inside [code] and [php] tags.
3964
 *
3965
 * @param string $code The code
3966
 * @return string The code with highlighted HTML.
3967
 */
3968
function highlight_php_code($code)
3969
{
3970
	// Remove special characters.
3971
	$code = un_htmlspecialchars(strtr($code, array('<br />' => "\n", '<br>' => "\n", "\t" => 'SMF_TAB();', '&#91;' => '[')));
3972
3973
	$oldlevel = error_reporting(0);
3974
3975
	$buffer = str_replace(array("\n", "\r"), '', @highlight_string($code, true));
3976
3977
	error_reporting($oldlevel);
3978
3979
	// Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
3980
	$buffer = preg_replace('~SMF_TAB(?:</(?:font|span)><(?:font color|span style)="[^"]*?">)?\\(\\);~', '<pre style="display: inline;">' . "\t" . '</pre>', $buffer);
3981
3982
	return strtr($buffer, array('\'' => '&#039;', '<code>' => '', '</code>' => ''));
3983
}
3984
3985
/**
3986
 * Gets the appropriate URL to use for images (or whatever) when using SSL
3987
 *
3988
 * The returned URL may or may not be a proxied URL, depending on the situation.
3989
 * Mods can implement alternative proxies using the 'integrate_proxy' hook.
3990
 *
3991
 * @param string $url The original URL of the requested resource
3992
 * @return string The URL to use
3993
 */
3994
function get_proxied_url($url)
3995
{
3996
	global $boardurl, $image_proxy_enabled, $image_proxy_secret, $user_info;
3997
3998
	// Only use the proxy if enabled, and never for robots
3999
	if (empty($image_proxy_enabled) || !empty($user_info['possibly_robot']))
4000
		return $url;
4001
4002
	$parsedurl = parse_iri($url);
4003
4004
	// Don't bother with HTTPS URLs, schemeless URLs, or obviously invalid URLs
4005
	if (empty($parsedurl['scheme']) || empty($parsedurl['host']) || empty($parsedurl['path']) || $parsedurl['scheme'] === 'https')
4006
		return $url;
4007
4008
	// We don't need to proxy our own resources
4009
	if ($parsedurl['host'] === parse_iri($boardurl, PHP_URL_HOST))
4010
		return strtr($url, array('http://' => 'https://'));
4011
4012
	// By default, use SMF's own image proxy script
4013
	$proxied_url = strtr($boardurl, array('http://' => 'https://')) . '/proxy.php?request=' . urlencode($url) . '&hash=' . hash_hmac('sha1', $url, $image_proxy_secret);
4014
4015
	// Allow mods to easily implement an alternative proxy
4016
	// MOD AUTHORS: To add settings UI for your proxy, use the integrate_general_settings hook.
4017
	call_integration_hook('integrate_proxy', array($url, &$proxied_url));
4018
4019
	return $proxied_url;
4020
}
4021
4022
/**
4023
 * Make sure the browser doesn't come back and repost the form data.
4024
 * Should be used whenever anything is posted.
4025
 *
4026
 * @param string $setLocation The URL to redirect them to
4027
 * @param bool $refresh Whether to use a meta refresh instead
4028
 * @param bool $permanent Whether to send a 301 Moved Permanently instead of a 302 Moved Temporarily
4029
 */
4030
function redirectexit($setLocation = '', $refresh = false, $permanent = false)
4031
{
4032
	global $scripturl, $context, $modSettings, $db_show_debug, $db_cache;
4033
4034
	// In case we have mail to send, better do that - as obExit doesn't always quite make it...
4035
	if (!empty($context['flush_mail']))
4036
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
4037
		AddMailQueue(true);
4038
4039
	$add = preg_match('~^(ftp|http)[s]?://~', $setLocation) == 0 && substr($setLocation, 0, 6) != 'about:';
4040
4041
	if ($add)
4042
		$setLocation = $scripturl . ($setLocation != '' ? '?' . $setLocation : '');
4043
4044
	// Put the session ID in.
4045
	if (defined('SID') && SID != '')
4046
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', $scripturl . '?' . SID . ';', $setLocation);
4047
	// Keep that debug in their for template debugging!
4048
	elseif (isset($_GET['debug']))
4049
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);
4050
4051
	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'])))
4052
	{
4053
		if (defined('SID') && SID != '')
4054
			$setLocation = preg_replace_callback(
4055
				'~^' . preg_quote($scripturl, '~') . '\?(?:' . SID . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~',
4056
				function($m) use ($scripturl)
4057
				{
4058
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . SID . (isset($m[2]) ? "$m[2]" : "");
4059
				},
4060
				$setLocation
4061
			);
4062
		else
4063
			$setLocation = preg_replace_callback(
4064
				'~^' . preg_quote($scripturl, '~') . '\?((?:board|topic)=[^#"]+?)(#[^"]*?)?$~',
4065
				function($m) use ($scripturl)
4066
				{
4067
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html' . (isset($m[2]) ? "$m[2]" : "");
4068
				},
4069
				$setLocation
4070
			);
4071
	}
4072
4073
	// Maybe integrations want to change where we are heading?
4074
	call_integration_hook('integrate_redirect', array(&$setLocation, &$refresh, &$permanent));
4075
4076
	// Set the header.
4077
	header('location: ' . str_replace(' ', '%20', $setLocation), true, $permanent ? 301 : 302);
4078
4079
	// Debugging.
4080
	if (isset($db_show_debug) && $db_show_debug === true)
4081
		$_SESSION['debug_redirect'] = $db_cache;
4082
4083
	obExit(false);
4084
}
4085
4086
/**
4087
 * Ends execution.  Takes care of template loading and remembering the previous URL.
4088
 *
4089
 * @param bool $header Whether to do the header
4090
 * @param bool $do_footer Whether to do the footer
4091
 * @param bool $from_index Whether we're coming from the board index
4092
 * @param bool $from_fatal_error Whether we're coming from a fatal error
4093
 */
4094
function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false)
4095
{
4096
	global $context, $settings, $modSettings, $txt, $smcFunc, $should_log;
4097
	static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
4098
4099
	// Attempt to prevent a recursive loop.
4100
	++$level;
4101
	if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
4102
		exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

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

Loading history...
4103
	if ($from_fatal_error)
4104
		$has_fatal_error = true;
4105
4106
	// Clear out the stat cache.
4107
	if (function_exists('trackStats'))
4108
		trackStats();
4109
4110
	// If we have mail to send, send it.
4111
	if (function_exists('AddMailQueue') && !empty($context['flush_mail']))
4112
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
4113
		AddMailQueue(true);
4114
4115
	$do_header = $header === null ? !$header_done : $header;
4116
	if ($do_footer === null)
4117
		$do_footer = $do_header;
4118
4119
	// Has the template/header been done yet?
4120
	if ($do_header)
4121
	{
4122
		// Was the page title set last minute? Also update the HTML safe one.
4123
		if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
4124
			$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](html_entity_decode($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
4125
4126
		// Start up the session URL fixer.
4127
		ob_start('ob_sessrewrite');
4128
4129
		if (!empty($settings['output_buffers']) && is_string($settings['output_buffers']))
4130
			$buffers = explode(',', $settings['output_buffers']);
4131
		elseif (!empty($settings['output_buffers']))
4132
			$buffers = $settings['output_buffers'];
4133
		else
4134
			$buffers = array();
4135
4136
		if (isset($modSettings['integrate_buffer']))
4137
			$buffers = array_merge(explode(',', $modSettings['integrate_buffer']), $buffers);
4138
4139
		if (!empty($buffers))
4140
			foreach ($buffers as $function)
4141
			{
4142
				$call = call_helper($function, true);
4143
4144
				// Is it valid?
4145
				if (!empty($call))
4146
					ob_start($call);
0 ignored issues
show
Bug introduced by
It seems like $call can also be of type boolean; however, parameter $callback of ob_start() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

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

4146
					ob_start(/** @scrutinizer ignore-type */ $call);
Loading history...
4147
			}
4148
4149
		// Display the screen in the logical order.
4150
		template_header();
4151
		$header_done = true;
4152
	}
4153
	if ($do_footer)
4154
	{
4155
		loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main');
4156
4157
		// Anything special to put out?
4158
		if (!empty($context['insert_after_template']) && !isset($_REQUEST['xml']))
4159
			echo $context['insert_after_template'];
4160
4161
		// Just so we don't get caught in an endless loop of errors from the footer...
4162
		if (!$footer_done)
4163
		{
4164
			$footer_done = true;
4165
			template_footer();
4166
4167
			// (since this is just debugging... it's okay that it's after </html>.)
4168
			if (!isset($_REQUEST['xml']))
4169
				displayDebug();
4170
		}
4171
	}
4172
4173
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
4174
	if ($should_log)
4175
		$_SESSION['old_url'] = $_SERVER['REQUEST_URL'];
4176
4177
	// For session check verification.... don't switch browsers...
4178
	$_SESSION['USER_AGENT'] = empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT'];
4179
4180
	// Hand off the output to the portal, etc. we're integrated with.
4181
	call_integration_hook('integrate_exit', array($do_footer));
4182
4183
	// Don't exit if we're coming from index.php; that will pass through normally.
4184
	if (!$from_index)
4185
		exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

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

Loading history...
4186
}
4187
4188
/**
4189
 * Get the size of a specified image with better error handling.
4190
 *
4191
 * @todo see if it's better in Subs-Graphics, but one step at the time.
4192
 * Uses getimagesize() to determine the size of a file.
4193
 * Attempts to connect to the server first so it won't time out.
4194
 *
4195
 * @param string $url The URL of the image
4196
 * @return array|false The image size as array (width, height), or false on failure
4197
 */
4198
function url_image_size($url)
4199
{
4200
	global $sourcedir;
4201
4202
	// Make sure it is a proper URL.
4203
	$url = str_replace(' ', '%20', $url);
4204
4205
	// Can we pull this from the cache... please please?
4206
	if (($temp = cache_get_data('url_image_size-' . md5($url), 240)) !== null)
4207
		return $temp;
4208
	$t = microtime(true);
4209
4210
	// Get the host to pester...
4211
	preg_match('~^\w+://(.+?)/(.*)$~', $url, $match);
4212
4213
	// Can't figure it out, just try the image size.
4214
	if ($url == '' || $url == 'http://' || $url == 'https://')
4215
	{
4216
		return false;
4217
	}
4218
	elseif (!isset($match[1]))
4219
	{
4220
		$size = @getimagesize($url);
4221
	}
4222
	else
4223
	{
4224
		// Try to connect to the server... give it half a second.
4225
		$temp = 0;
4226
		$fp = @fsockopen($match[1], 80, $temp, $temp, 0.5);
4227
4228
		// Successful?  Continue...
4229
		if ($fp != false)
4230
		{
4231
			// Send the HEAD request (since we don't have to worry about chunked, HTTP/1.1 is fine here.)
4232
			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");
4233
4234
			// Read in the HTTP/1.1 or whatever.
4235
			$test = substr(fgets($fp, 11), -1);
4236
			fclose($fp);
4237
4238
			// See if it returned a 404/403 or something.
4239
			if ($test < 4)
4240
			{
4241
				$size = @getimagesize($url);
4242
4243
				// This probably means allow_url_fopen is off, let's try GD.
4244
				if ($size === false && function_exists('imagecreatefromstring'))
4245
				{
4246
					// It's going to hate us for doing this, but another request...
4247
					$image = @imagecreatefromstring(fetch_web_data($url));
0 ignored issues
show
Bug introduced by
It seems like fetch_web_data($url) can also be of type false; however, parameter $image of imagecreatefromstring() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

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

5239
		if (strpos(/** @scrutinizer ignore-type */ $test, 'not found') !== false)
Loading history...
5240
			$host = '';
5241
		// Invalid server option?
5242
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
5243
			updateSettings(array('host_to_dis' => 1));
5244
		// Maybe it found something, after all?
5245
		elseif (preg_match('~\s([^\s]+?)\.\s~', $test, $match) == 1)
0 ignored issues
show
Bug introduced by
It seems like $test can also be of type false 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

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

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

5759
			$results[$function] = call_user_func_array(/** @scrutinizer ignore-type */ $call, $parameters);
Loading history...
5760
		// This failed, but we want to do so silently.
5761
		elseif (!empty($function) && !empty($context['ignore_hook_errors']))
5762
			return $results;
5763
		// Whatever it was suppose to call, it failed :(
5764
		elseif (!empty($function))
5765
		{
5766
			loadLanguage('Errors');
5767
5768
			// Get a full path to show on error.
5769
			if (strpos($function, '|') !== false)
5770
			{
5771
				list ($file, $string) = explode('|', $function);
5772
				$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'])));
5773
				log_error(sprintf($txt['hook_fail_call_to'], $string, $absPath), 'general');
5774
			}
5775
			// "Assume" the file resides on $boarddir somewhere...
5776
			else
5777
				log_error(sprintf($txt['hook_fail_call_to'], $function, $boarddir), 'general');
5778
		}
5779
	}
5780
5781
	return $results;
5782
}
5783
5784
/**
5785
 * Add a function for integration hook.
5786
 * does nothing if the function is already added.
5787
 *
5788
 * @param string $hook The complete hook name.
5789
 * @param string $function The function name. Can be a call to a method via Class::method.
5790
 * @param bool $permanent If true, updates the value in settings table.
5791
 * @param string $file The file. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
5792
 * @param bool $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
5793
 */
5794
function add_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
5795
{
5796
	global $smcFunc, $modSettings;
5797
5798
	// Any objects?
5799
	if ($object)
5800
		$function = $function . '#';
5801
5802
	// Any files  to load?
5803
	if (!empty($file) && is_string($file))
5804
		$function = $file . (!empty($function) ? '|' . $function : '');
5805
5806
	// Get the correct string.
5807
	$integration_call = $function;
5808
5809
	// Is it going to be permanent?
5810
	if ($permanent)
5811
	{
5812
		$request = $smcFunc['db_query']('', '
5813
			SELECT value
5814
			FROM {db_prefix}settings
5815
			WHERE variable = {string:variable}',
5816
			array(
5817
				'variable' => $hook,
5818
			)
5819
		);
5820
		list ($current_functions) = $smcFunc['db_fetch_row']($request);
5821
		$smcFunc['db_free_result']($request);
5822
5823
		if (!empty($current_functions))
5824
		{
5825
			$current_functions = explode(',', $current_functions);
5826
			if (in_array($integration_call, $current_functions))
5827
				return;
5828
5829
			$permanent_functions = array_merge($current_functions, array($integration_call));
5830
		}
5831
		else
5832
			$permanent_functions = array($integration_call);
5833
5834
		updateSettings(array($hook => implode(',', $permanent_functions)));
5835
	}
5836
5837
	// Make current function list usable.
5838
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
5839
5840
	// Do nothing, if it's already there.
5841
	if (in_array($integration_call, $functions))
5842
		return;
5843
5844
	$functions[] = $integration_call;
5845
	$modSettings[$hook] = implode(',', $functions);
5846
}
5847
5848
/**
5849
 * Remove an integration hook function.
5850
 * Removes the given function from the given hook.
5851
 * Does nothing if the function is not available.
5852
 *
5853
 * @param string $hook The complete hook name.
5854
 * @param string $function The function name. Can be a call to a method via Class::method.
5855
 * @param boolean $permanent Irrelevant for the function itself but need to declare it to match
5856
 * @param string $file The filename. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
5857
 * @param boolean $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
5858
 * @see add_integration_function
5859
 */
5860
function remove_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
5861
{
5862
	global $smcFunc, $modSettings;
5863
5864
	// Any objects?
5865
	if ($object)
5866
		$function = $function . '#';
5867
5868
	// Any files  to load?
5869
	if (!empty($file) && is_string($file))
5870
		$function = $file . '|' . $function;
5871
5872
	// Get the correct string.
5873
	$integration_call = $function;
5874
5875
	// Get the permanent functions.
5876
	$request = $smcFunc['db_query']('', '
5877
		SELECT value
5878
		FROM {db_prefix}settings
5879
		WHERE variable = {string:variable}',
5880
		array(
5881
			'variable' => $hook,
5882
		)
5883
	);
5884
	list ($current_functions) = $smcFunc['db_fetch_row']($request);
5885
	$smcFunc['db_free_result']($request);
5886
5887
	if (!empty($current_functions))
5888
	{
5889
		$current_functions = explode(',', $current_functions);
5890
5891
		if (in_array($integration_call, $current_functions))
5892
			updateSettings(array($hook => implode(',', array_diff($current_functions, array($integration_call)))));
5893
	}
5894
5895
	// Turn the function list into something usable.
5896
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
5897
5898
	// You can only remove it if it's available.
5899
	if (!in_array($integration_call, $functions))
5900
		return;
5901
5902
	$functions = array_diff($functions, array($integration_call));
5903
	$modSettings[$hook] = implode(',', $functions);
5904
}
5905
5906
/**
5907
 * Receives a string and tries to figure it out if its a method or a function.
5908
 * If a method is found, it looks for a "#" which indicates SMF should create a new instance of the given class.
5909
 * Checks the string/array for is_callable() and return false/fatal_lang_error is the given value results in a non callable string/array.
5910
 * Prepare and returns a callable depending on the type of method/function found.
5911
 *
5912
 * @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)
5913
 * @param boolean $return If true, the function will not call the function/method but instead will return the formatted string.
5914
 * @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.
5915
 */
5916
function call_helper($string, $return = false)
5917
{
5918
	global $context, $smcFunc, $txt, $db_show_debug;
5919
5920
	// Really?
5921
	if (empty($string))
5922
		return false;
5923
5924
	// An array? should be a "callable" array IE array(object/class, valid_callable).
5925
	// A closure? should be a callable one.
5926
	if (is_array($string) || $string instanceof Closure)
5927
		return $return ? $string : (is_callable($string) ? call_user_func($string) : false);
5928
5929
	// No full objects, sorry! pass a method or a property instead!
5930
	if (is_object($string))
5931
		return false;
5932
5933
	// Stay vitaminized my friends...
5934
	$string = $smcFunc['htmlspecialchars']($smcFunc['htmltrim']($string));
5935
5936
	// Is there a file to load?
5937
	$string = load_file($string);
5938
5939
	// Loaded file failed
5940
	if (empty($string))
5941
		return false;
5942
5943
	// Found a method.
5944
	if (strpos($string, '::') !== false)
0 ignored issues
show
Bug introduced by
It seems like $string can also be of type boolean; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

5944
	if (strpos(/** @scrutinizer ignore-type */ $string, '::') !== false)
Loading history...
5945
	{
5946
		list ($class, $method) = explode('::', $string);
0 ignored issues
show
Bug introduced by
It seems like $string can also be of type boolean; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

6230
			$fetch_data->get_url_data($url, /** @scrutinizer ignore-type */ $post_data);
Loading history...
6231
6232
			// no errors and a 200 result, then we have a good dataset, well we at least have data. ;)
6233
			if ($fetch_data->result('code') == 200 && !$fetch_data->result('error'))
6234
				$data = $fetch_data->result('body');
6235
			else
6236
				return false;
6237
		}
6238
6239
		// Neither fsockopen nor curl are available. Well, phooey.
6240
		else
6241
			return false;
6242
	}
6243
	else
6244
	{
6245
		// Umm, this shouldn't happen?
6246
		loadLanguage('Errors');
6247
		trigger_error($txt['fetch_web_data_bad_url'], E_USER_NOTICE);
6248
		$data = false;
6249
	}
6250
6251
	return $data;
6252
}
6253
6254
/**
6255
 * Attempts to determine the MIME type of some data or a file.
6256
 *
6257
 * @param string $data The data to check, or the path or URL of a file to check.
6258
 * @param string $is_path If true, $data is a path or URL to a file.
6259
 * @return string|bool A MIME type, or false if we cannot determine it.
6260
 */
6261
function get_mime_type($data, $is_path = false)
6262
{
6263
	global $cachedir;
6264
6265
	$finfo_loaded = extension_loaded('fileinfo');
6266
	$exif_loaded = extension_loaded('exif') && function_exists('image_type_to_mime_type');
6267
6268
	// Oh well. We tried.
6269
	if (!$finfo_loaded && !$exif_loaded)
6270
		return false;
6271
6272
	// Start with the 'empty' MIME type.
6273
	$mime_type = 'application/x-empty';
6274
6275
	if ($finfo_loaded)
6276
	{
6277
		// Just some nice, simple data to analyze.
6278
		if (empty($is_path))
6279
			$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
6280
6281
		// A file, or maybe a URL?
6282
		else
6283
		{
6284
			// Local file.
6285
			if (file_exists($data))
6286
				$mime_type = mime_content_type($data);
6287
6288
			// URL.
6289
			elseif ($data = fetch_web_data($data))
6290
				$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
6291
		}
6292
	}
6293
	// Workaround using Exif requires a local file.
6294
	else
6295
	{
6296
		// If $data is a URL to fetch, do so.
6297
		if (!empty($is_path) && !file_exists($data) && url_exists($data))
6298
		{
6299
			$data = fetch_web_data($data);
6300
			$is_path = false;
6301
		}
6302
6303
		// If we don't have a local file, create one and use it.
6304
		if (empty($is_path))
6305
		{
6306
			$temp_file = tempnam($cachedir, md5($data));
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type false; however, parameter $string of md5() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

6306
			$temp_file = tempnam($cachedir, md5(/** @scrutinizer ignore-type */ $data));
Loading history...
6307
			file_put_contents($temp_file, $data);
6308
			$is_path = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $is_path is dead and can be removed.
Loading history...
6309
			$data = $temp_file;
6310
		}
6311
6312
		$imagetype = @exif_imagetype($data);
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type false; however, parameter $filename of exif_imagetype() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

6312
		$imagetype = @exif_imagetype(/** @scrutinizer ignore-type */ $data);
Loading history...
6313
6314
		if (isset($temp_file))
6315
			unlink($temp_file);
6316
6317
		// Unfortunately, this workaround only works for image files.
6318
		if ($imagetype !== false)
6319
			$mime_type = image_type_to_mime_type($imagetype);
6320
	}
6321
6322
	return $mime_type;
6323
}
6324
6325
/**
6326
 * Checks whether a file or data has the expected MIME type.
6327
 *
6328
 * @param string $data The data to check, or the path or URL of a file to check.
6329
 * @param string $type_pattern A regex pattern to match the acceptable MIME types.
6330
 * @param string $is_path If true, $data is a path or URL to a file.
6331
 * @return int 1 if the detected MIME type matches the pattern, 0 if it doesn't, or 2 if we can't check.
6332
 */
6333
function check_mime_type($data, $type_pattern, $is_path = false)
6334
{
6335
	// Get the MIME type.
6336
	$mime_type = get_mime_type($data, $is_path);
0 ignored issues
show
Bug introduced by
It seems like $is_path can also be of type false; however, parameter $is_path of get_mime_type() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

6336
	$mime_type = get_mime_type($data, /** @scrutinizer ignore-type */ $is_path);
Loading history...
6337
6338
	// Couldn't determine it.
6339
	if ($mime_type === false)
6340
		return 2;
6341
6342
	// Check whether the MIME type matches expectations.
6343
	return (int) @preg_match('~' . $type_pattern . '~', $mime_type);
6344
}
6345
6346
/**
6347
 * Prepares an array of "likes" info for the topic specified by $topic
6348
 *
6349
 * @param integer $topic The topic ID to fetch the info from.
6350
 * @return array An array of IDs of messages in the specified topic that the current user likes
6351
 */
6352
function prepareLikesContext($topic)
6353
{
6354
	global $user_info, $smcFunc;
6355
6356
	// Make sure we have something to work with.
6357
	if (empty($topic))
6358
		return array();
6359
6360
	// We already know the number of likes per message, we just want to know whether the current user liked it or not.
6361
	$user = $user_info['id'];
6362
	$cache_key = 'likes_topic_' . $topic . '_' . $user;
6363
	$ttl = 180;
6364
6365
	if (($temp = cache_get_data($cache_key, $ttl)) === null)
6366
	{
6367
		$temp = array();
6368
		$request = $smcFunc['db_query']('', '
6369
			SELECT content_id
6370
			FROM {db_prefix}user_likes AS l
6371
				INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg)
6372
			WHERE l.id_member = {int:current_user}
6373
				AND l.content_type = {literal:msg}
6374
				AND m.id_topic = {int:topic}',
6375
			array(
6376
				'current_user' => $user,
6377
				'topic' => $topic,
6378
			)
6379
		);
6380
		while ($row = $smcFunc['db_fetch_assoc']($request))
6381
			$temp[] = (int) $row['content_id'];
6382
6383
		cache_put_data($cache_key, $temp, $ttl);
6384
	}
6385
6386
	return $temp;
6387
}
6388
6389
/**
6390
 * Microsoft uses their own character set Code Page 1252 (CP1252), which is a
6391
 * superset of ISO 8859-1, defining several characters between DEC 128 and 159
6392
 * that are not normally displayable.  This converts the popular ones that
6393
 * appear from a cut and paste from windows.
6394
 *
6395
 * @param string $string The string
6396
 * @return string The sanitized string
6397
 */
6398
function sanitizeMSCutPaste($string)
6399
{
6400
	global $context;
6401
6402
	if (empty($string))
6403
		return $string;
6404
6405
	// UTF-8 occurences of MS special characters
6406
	$findchars_utf8 = array(
6407
		"\xe2\x80\x9a",	// single low-9 quotation mark
6408
		"\xe2\x80\x9e",	// double low-9 quotation mark
6409
		"\xe2\x80\xa6",	// horizontal ellipsis
6410
		"\xe2\x80\x98",	// left single curly quote
6411
		"\xe2\x80\x99",	// right single curly quote
6412
		"\xe2\x80\x9c",	// left double curly quote
6413
		"\xe2\x80\x9d",	// right double curly quote
6414
	);
6415
6416
	// windows 1252 / iso equivalents
6417
	$findchars_iso = array(
6418
		chr(130),
6419
		chr(132),
6420
		chr(133),
6421
		chr(145),
6422
		chr(146),
6423
		chr(147),
6424
		chr(148),
6425
	);
6426
6427
	// safe replacements
6428
	$replacechars = array(
6429
		',',	// &sbquo;
6430
		',,',	// &bdquo;
6431
		'...',	// &hellip;
6432
		"'",	// &lsquo;
6433
		"'",	// &rsquo;
6434
		'"',	// &ldquo;
6435
		'"',	// &rdquo;
6436
	);
6437
6438
	if ($context['utf8'])
6439
		$string = str_replace($findchars_utf8, $replacechars, $string);
6440
	else
6441
		$string = str_replace($findchars_iso, $replacechars, $string);
6442
6443
	return $string;
6444
}
6445
6446
/**
6447
 * Decode numeric html entities to their ascii or UTF8 equivalent character.
6448
 *
6449
 * Callback function for preg_replace_callback in subs-members
6450
 * Uses capture group 2 in the supplied array
6451
 * Does basic scan to ensure characters are inside a valid range
6452
 *
6453
 * @param array $matches An array of matches (relevant info should be the 3rd item)
6454
 * @return string A fixed string
6455
 */
6456
function replaceEntities__callback($matches)
6457
{
6458
	global $context;
6459
6460
	if (!isset($matches[2]))
6461
		return '';
6462
6463
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
0 ignored issues
show
Bug introduced by
$matches[2] of type array is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

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

6463
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
6464
6465
	// remove left to right / right to left overrides
6466
	if ($num === 0x202D || $num === 0x202E)
6467
		return '';
6468
6469
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
6470
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
6471
		return '&#' . $num . ';';
6472
6473
	if (empty($context['utf8']))
6474
	{
6475
		// no control characters
6476
		if ($num < 0x20)
6477
			return '';
6478
		// text is text
6479
		elseif ($num < 0x80)
6480
			return chr($num);
0 ignored issues
show
Bug introduced by
It seems like $num can also be of type double; however, parameter $codepoint of chr() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

6483
			return '&#' . /** @scrutinizer ignore-type */ $matches[2] . ';';
Loading history...
6484
	}
6485
	else
6486
	{
6487
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
6488
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
6489
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
6490
			return '';
6491
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
6492
		elseif ($num < 0x80)
6493
			return chr($num);
6494
		// <0x800 (2048)
6495
		elseif ($num < 0x800)
6496
			return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
6497
		// < 0x10000 (65536)
6498
		elseif ($num < 0x10000)
6499
			return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6500
		// <= 0x10FFFF (1114111)
6501
		else
6502
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6503
	}
6504
}
6505
6506
/**
6507
 * Converts html entities to utf8 equivalents
6508
 *
6509
 * Callback function for preg_replace_callback
6510
 * Uses capture group 1 in the supplied array
6511
 * Does basic checks to keep characters inside a viewable range.
6512
 *
6513
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
6514
 * @return string The fixed string
6515
 */
6516
function fixchar__callback($matches)
6517
{
6518
	if (!isset($matches[1]))
6519
		return '';
6520
6521
	$num = $matches[1][0] === 'x' ? hexdec(substr($matches[1], 1)) : (int) $matches[1];
0 ignored issues
show
Bug introduced by
$matches[1] of type array is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

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

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

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

6529
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
6530
	// <0x800 (2048)
6531
	elseif ($num < 0x800)
6532
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
6533
	// < 0x10000 (65536)
6534
	elseif ($num < 0x10000)
6535
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6536
	// <= 0x10FFFF (1114111)
6537
	else
6538
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6539
}
6540
6541
/**
6542
 * Strips out invalid html entities, replaces others with html style &#123; codes
6543
 *
6544
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
6545
 * strpos, strlen, substr etc
6546
 *
6547
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
6548
 * @return string The fixed string
6549
 */
6550
function entity_fix__callback($matches)
6551
{
6552
	if (!isset($matches[2]))
6553
		return '';
6554
6555
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
0 ignored issues
show
Bug introduced by
$matches[2] of type array is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

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

6555
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
6556
6557
	// we don't allow control characters, characters out of range, byte markers, etc
6558
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
6559
		return '';
6560
	else
6561
		return '&#' . $num . ';';
6562
}
6563
6564
/**
6565
 * Return a Gravatar URL based on
6566
 * - the supplied email address,
6567
 * - the global maximum rating,
6568
 * - the global default fallback,
6569
 * - maximum sizes as set in the admin panel.
6570
 *
6571
 * It is SSL aware, and caches most of the parameters.
6572
 *
6573
 * @param string $email_address The user's email address
6574
 * @return string The gravatar URL
6575
 */
6576
function get_gravatar_url($email_address)
6577
{
6578
	global $modSettings, $smcFunc;
6579
	static $url_params = null;
6580
6581
	if ($url_params === null)
6582
	{
6583
		$ratings = array('G', 'PG', 'R', 'X');
6584
		$defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank');
6585
		$url_params = array();
6586
		if (!empty($modSettings['gravatarMaxRating']) && in_array($modSettings['gravatarMaxRating'], $ratings))
6587
			$url_params[] = 'rating=' . $modSettings['gravatarMaxRating'];
6588
		if (!empty($modSettings['gravatarDefault']) && in_array($modSettings['gravatarDefault'], $defaults))
6589
			$url_params[] = 'default=' . $modSettings['gravatarDefault'];
6590
		if (!empty($modSettings['avatar_max_width_external']))
6591
			$size_string = (int) $modSettings['avatar_max_width_external'];
6592
		if (!empty($modSettings['avatar_max_height_external']) && !empty($size_string))
6593
			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...
6594
				$size_string = $modSettings['avatar_max_height_external'];
6595
6596
		if (!empty($size_string))
6597
			$url_params[] = 's=' . $size_string;
6598
	}
6599
	$http_method = !empty($modSettings['force_ssl']) ? 'https://secure' : 'http://www';
6600
6601
	return $http_method . '.gravatar.com/avatar/' . md5($smcFunc['strtolower']($email_address)) . '?' . implode('&', $url_params);
6602
}
6603
6604
/**
6605
 * Get a list of time zones.
6606
 *
6607
 * @param string $when The date/time for which to calculate the time zone values.
6608
 *		May be a Unix timestamp or any string that strtotime() can understand.
6609
 *		Defaults to 'now'.
6610
 * @return array An array of time zone identifiers and label text.
6611
 */
6612
function smf_list_timezones($when = 'now')
6613
{
6614
	global $modSettings, $tztxt, $txt, $context, $cur_profile, $sourcedir;
6615
	static $timezones_when = array();
6616
6617
	require_once($sourcedir . '/Subs-Timezones.php');
6618
6619
	// Parseable datetime string?
6620
	if (is_int($timestamp = strtotime($when)))
0 ignored issues
show
introduced by
The condition is_int($timestamp = strtotime($when)) is always true.
Loading history...
6621
		$when = $timestamp;
6622
6623
	// A Unix timestamp?
6624
	elseif (is_numeric($when))
6625
		$when = intval($when);
6626
6627
	// Invalid value? Just get current Unix timestamp.
6628
	else
6629
		$when = time();
6630
6631
	// No point doing this over if we already did it once
6632
	if (isset($timezones_when[$when]))
6633
		return $timezones_when[$when];
6634
6635
	// We'll need these too
6636
	$date_when = date_create('@' . $when);
6637
	$later = strtotime('@' . $when . ' + 1 year');
6638
6639
	// Load up any custom time zone descriptions we might have
6640
	loadLanguage('Timezones');
6641
6642
	$tzid_metazones = get_tzid_metazones($later);
6643
6644
	// Should we put time zones from certain countries at the top of the list?
6645
	$priority_countries = !empty($modSettings['timezone_priority_countries']) ? explode(',', $modSettings['timezone_priority_countries']) : array();
6646
6647
	$priority_tzids = array();
6648
	foreach ($priority_countries as $country)
6649
	{
6650
		$country_tzids = get_sorted_tzids_for_country($country);
6651
6652
		if (!empty($country_tzids))
6653
			$priority_tzids = array_merge($priority_tzids, $country_tzids);
6654
	}
6655
6656
	// Antarctic research stations should be listed last, unless you're running a penguin forum
6657
	$low_priority_tzids = !in_array('AQ', $priority_countries) ? timezone_identifiers_list(DateTimeZone::ANTARCTICA) : array();
0 ignored issues
show
Bug introduced by
Are you sure the usage of timezone_identifiers_lis...teTimeZone::ANTARCTICA) is correct as it seems to always return null.

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

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

}

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

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

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

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

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

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

}

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

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

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

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

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

6659
	$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...
6660
6661
	// Process them in order of importance.
6662
	$tzids = array_merge($priority_tzids, $normal_priority_tzids, $low_priority_tzids);
6663
6664
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
6665
	$dst_types = array();
6666
	$labels = array();
6667
	$offsets = array();
6668
	foreach ($tzids as $tzid)
6669
	{
6670
		// We don't want UTC right now
6671
		if ($tzid == 'UTC')
6672
			continue;
6673
6674
		$tz = @timezone_open($tzid);
6675
6676
		if ($tz == null)
6677
			continue;
6678
6679
		// First, get the set of transition rules for this tzid
6680
		$tzinfo = timezone_transitions_get($tz, $when, $later);
6681
6682
		// Use the entire set of transition rules as the array *key* so we can avoid duplicates
6683
		$tzkey = serialize($tzinfo);
6684
6685
		// ...But make sure to include all explicitly defined meta-zones.
6686
		if (isset($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid]))
6687
			$tzkey = serialize(array_merge($tzinfo, array('metazone' => $tzid_metazones[$tzid])));
6688
6689
		// Don't overwrite our preferred tzids
6690
		if (empty($zones[$tzkey]['tzid']))
6691
		{
6692
			$zones[$tzkey]['tzid'] = $tzid;
6693
			$zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
6694
6695
			foreach ($tzinfo as $transition) {
6696
				$zones[$tzkey]['abbrs'][] = $transition['abbr'];
6697
			}
6698
6699
			if (isset($tzid_metazones[$tzid]))
6700
				$zones[$tzkey]['metazone'] = $tzid_metazones[$tzid];
6701
			else
6702
			{
6703
				$tzgeo = timezone_location_get($tz);
6704
				$country_tzids = get_sorted_tzids_for_country($tzgeo['country_code']);
6705
6706
				if (count($country_tzids) === 1)
6707
					$zones[$tzkey]['metazone'] = $txt['iso3166'][$tzgeo['country_code']];
6708
			}
6709
		}
6710
6711
		// A time zone from a prioritized country?
6712
		if (in_array($tzid, $priority_tzids))
6713
			$priority_zones[$tzkey] = true;
6714
6715
		// Keep track of the location for this tzid.
6716
		if (!empty($txt[$tzid]))
6717
			$zones[$tzkey]['locations'][] = $txt[$tzid];
6718
		else
6719
		{
6720
			$tzid_parts = explode('/', $tzid);
6721
			$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
6722
		}
6723
6724
		// Keep track of the current offset for this tzid.
6725
		$offsets[$tzkey] = $tzinfo[0]['offset'];
6726
6727
		// Keep track of the Standard Time offset for this tzid.
6728
		foreach ($tzinfo as $transition)
6729
		{
6730
			if (!$transition['isdst'])
6731
			{
6732
				$std_offsets[$tzkey] = $transition['offset'];
6733
				break;
6734
			}
6735
		}
6736
		if (!isset($std_offsets[$tzkey]))
6737
			$std_offsets[$tzkey] = $tzinfo[0]['offset'];
6738
6739
		// Figure out the "meta-zone" info for the label
6740
		if (empty($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid]))
6741
		{
6742
			$zones[$tzkey]['metazone'] = $tzid_metazones[$tzid];
6743
			$zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
6744
		}
6745
		$dst_types[$tzkey] = count($tzinfo) > 1 ? 'c' : ($tzinfo[0]['isdst'] ? 't' : 'f');
6746
		$labels[$tzkey] = !empty($zones[$tzkey]['metazone']) && !empty($tztxt[$zones[$tzkey]['metazone']]) ? $tztxt[$zones[$tzkey]['metazone']] : '';
6747
6748
		// Remember this for later
6749
		if (isset($cur_profile['timezone']) && $cur_profile['timezone'] == $tzid)
6750
			$member_tzkey = $tzkey;
6751
		if (isset($context['event']['tz']) && $context['event']['tz'] == $tzid)
6752
			$event_tzkey = $tzkey;
6753
		if ($modSettings['default_timezone'] == $tzid)
6754
			$default_tzkey = $tzkey;
6755
	}
6756
6757
	// Sort by current offset, then standard offset, then DST type, then label.
6758
	array_multisort($offsets, SORT_DESC, SORT_NUMERIC, $std_offsets, SORT_DESC, SORT_NUMERIC, $dst_types, SORT_ASC, $labels, SORT_ASC, $zones);
0 ignored issues
show
Bug introduced by
SORT_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

6758
	array_multisort($offsets, SORT_DESC, /** @scrutinizer ignore-type */ SORT_NUMERIC, $std_offsets, SORT_DESC, SORT_NUMERIC, $dst_types, SORT_ASC, $labels, SORT_ASC, $zones);
Loading history...
Comprehensibility Best Practice introduced by
The variable $zones does not seem to be defined for all execution paths leading up to this point.
Loading history...
Bug introduced by
SORT_DESC cannot be passed to array_multisort() as the parameter $rest expects a reference. ( Ignorable by Annotation )

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

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

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

6758
	array_multisort($offsets, SORT_DESC, SORT_NUMERIC, $std_offsets, SORT_DESC, SORT_NUMERIC, $dst_types, /** @scrutinizer ignore-type */ SORT_ASC, $labels, SORT_ASC, $zones);
Loading history...
Comprehensibility Best Practice introduced by
The variable $std_offsets does not seem to be defined for all execution paths leading up to this point.
Loading history...
6759
6760
	// Build the final array of formatted values
6761
	$priority_timezones = array();
6762
	$timezones = array();
6763
	foreach ($zones as $tzkey => $tzvalue)
6764
	{
6765
		date_timezone_set($date_when, timezone_open($tzvalue['tzid']));
6766
6767
		// Use the human friendly time zone name, if there is one.
6768
		$desc = '';
6769
		if (!empty($tzvalue['metazone']))
6770
		{
6771
			if (!empty($tztxt[$tzvalue['metazone']]))
6772
				$metazone = $tztxt[$tzvalue['metazone']];
6773
			else
6774
				$metazone = sprintf($tztxt['generic_timezone'], $tzvalue['metazone'], '%1$s');
6775
6776
			switch ($tzvalue['dst_type'])
6777
			{
6778
				case 0:
6779
					$desc = sprintf($metazone, $tztxt['daylight_saving_time_false']);
6780
					break;
6781
6782
				case 1:
6783
					$desc = sprintf($metazone, '');
6784
					break;
6785
6786
				case 2:
6787
					$desc = sprintf($metazone, $tztxt['daylight_saving_time_true']);
6788
					break;
6789
			}
6790
		}
6791
		// Otherwise, use the list of locations (max 5, so things don't get silly)
6792
		else
6793
			$desc = implode(', ', array_slice(array_unique($tzvalue['locations']), 0, 5)) . (count($tzvalue['locations']) > 5 ? ', ' . $txt['etc'] : '');
6794
6795
		// We don't want abbreviations like '+03' or '-11'.
6796
		$abbrs = array_filter(
6797
			$tzvalue['abbrs'],
6798
			function ($abbr)
6799
			{
6800
				return !strspn($abbr, '+-');
6801
			}
6802
		);
6803
		$abbrs = count($abbrs) == count($tzvalue['abbrs']) ? array_unique($abbrs) : array();
6804
6805
		// Show the UTC offset and abbreviation(s).
6806
		$desc = '[UTC' . date_format($date_when, 'P') . '] - ' . str_replace('  ', ' ', $desc) . (!empty($abbrs) ? ' (' . implode('/', $abbrs) . ')' : '');
6807
6808
		if (isset($priority_zones[$tzkey]))
6809
			$priority_timezones[$tzvalue['tzid']] = $desc;
6810
		else
6811
			$timezones[$tzvalue['tzid']] = $desc;
6812
6813
		// Automatically fix orphaned time zones.
6814
		if (isset($member_tzkey) && $member_tzkey == $tzkey)
6815
			$cur_profile['timezone'] = $tzvalue['tzid'];
6816
		if (isset($event_tzkey) && $event_tzkey == $tzkey)
6817
			$context['event']['tz'] = $tzvalue['tzid'];
6818
		if (isset($default_tzkey) && $default_tzkey == $tzkey && $modSettings['default_timezone'] != $tzvalue['tzid'])
6819
			updateSettings(array('default_timezone' => $tzvalue['tzid']));
6820
	}
6821
6822
	if (!empty($priority_timezones))
6823
		$priority_timezones[] = '-----';
6824
6825
	$timezones = array_merge(
6826
		$priority_timezones,
6827
		array('UTC' => 'UTC' . (!empty($tztxt['UTC']) ? ' - ' . $tztxt['UTC'] : ''), '-----'),
6828
		$timezones
6829
	);
6830
6831
	$timezones_when[$when] = $timezones;
6832
6833
	return $timezones_when[$when];
6834
}
6835
6836
/**
6837
 * Gets a member's selected time zone identifier
6838
 *
6839
 * @param int $id_member The member id to look up. If not provided, the current user's id will be used.
6840
 * @return string The time zone identifier string for the user's time zone.
6841
 */
6842
function getUserTimezone($id_member = null)
6843
{
6844
	global $smcFunc, $user_info, $modSettings, $user_settings;
6845
	static $member_cache = array();
6846
6847
	if (is_null($id_member))
6848
		$id_member = empty($user_info['id']) ? 0 : (int) $user_info['id'];
6849
	else
6850
		$id_member = (int) $id_member;
6851
6852
	// Did we already look this up?
6853
	if (isset($member_cache[$id_member]))
6854
		return $member_cache[$id_member];
6855
6856
	// Check if we already have this in $user_settings.
6857
	if (isset($user_settings['id_member']) && $user_settings['id_member'] == $id_member && !empty($user_settings['timezone']))
6858
	{
6859
		$member_cache[$id_member] = $user_settings['timezone'];
6860
		return $user_settings['timezone'];
6861
	}
6862
6863
	if (!empty($id_member))
6864
	{
6865
		// Look it up in the database.
6866
		$request = $smcFunc['db_query']('', '
6867
			SELECT timezone
6868
			FROM {db_prefix}members
6869
			WHERE id_member = {int:id_member}',
6870
			array(
6871
				'id_member' => $id_member,
6872
			)
6873
		);
6874
		list($timezone) = $smcFunc['db_fetch_row']($request);
6875
		$smcFunc['db_free_result']($request);
6876
	}
6877
6878
	// If it is invalid, fall back to the default.
6879
	if (empty($timezone) || !in_array($timezone, timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)))
0 ignored issues
show
Bug introduced by
timezone_identifiers_lis...eTimeZone::ALL_WITH_BC) of type void is incompatible with the type array expected by parameter $haystack of in_array(). ( Ignorable by Annotation )

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

6879
	if (empty($timezone) || !in_array($timezone, /** @scrutinizer ignore-type */ timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)))
Loading history...
Bug introduced by
Are you sure the usage of timezone_identifiers_lis...eTimeZone::ALL_WITH_BC) is correct as it seems to always return null.

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

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

}

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

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

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

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

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

7385
		if (md5(/** @scrutinizer ignore-type */ $tlds) != substr($tlds_md5, 0, 32))
Loading history...
Bug introduced by
It seems like $tlds_md5 can also be of type false; however, parameter $string of substr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

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

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

8266
				$value = substr($value, 0, /** @scrutinizer ignore-type */ $param_max - strlen($key) - 5);
Loading history...
8267
		}
8268
8269
		return $array;
8270
	}
8271
}
8272
8273
/**
8274
 * array_length Recursive
8275
 * @param array $array
8276
 * @param int $deep How many levels should the function
8277
 * @return int
8278
 */
8279
function array_length($array, $deep = 3)
8280
{
8281
	// Work with arrays
8282
	$array = (array) $array;
8283
	$length = 0;
8284
8285
	$deep_count = $deep - 1;
8286
8287
	foreach ($array as $value)
8288
	{
8289
		// Recursive?
8290
		if (is_array($value))
8291
		{
8292
			// No can't do
8293
			if ($deep_count <= 0)
8294
				continue;
8295
8296
			$length += array_length($value, $deep_count);
8297
		}
8298
		else
8299
			$length += strlen($value);
8300
	}
8301
8302
	return $length;
8303
}
8304
8305
/**
8306
 * Compares existance request variables against an array.
8307
 *
8308
 * The input array is associative, where keys denote accepted values
8309
 * in a request variable denoted by `$req_val`. Values can be:
8310
 *
8311
 * - another associative array where at least one key must be found
8312
 *   in the request and their values are accepted request values.
8313
 * - A scalar value, in which case no furthur checks are done.
8314
 *
8315
 * @param array $array
8316
 * @param string $req_var request variable
8317
 *
8318
 * @return bool whether any of the criteria was satisfied
8319
 */
8320
function is_filtered_request(array $array, $req_var)
8321
{
8322
	$matched = false;
8323
	if (isset($_REQUEST[$req_var], $array[$_REQUEST[$req_var]]))
8324
	{
8325
		if (is_array($array[$_REQUEST[$req_var]]))
8326
		{
8327
			foreach ($array[$_REQUEST[$req_var]] as $subtype => $subnames)
8328
				$matched |= isset($_REQUEST[$subtype]) && in_array($_REQUEST[$subtype], $subnames);
8329
		}
8330
		else
8331
			$matched = true;
8332
	}
8333
8334
	return (bool) $matched;
8335
}
8336
8337
/**
8338
 * Clean up the XML to make sure it doesn't contain invalid characters.
8339
 *
8340
 * See https://www.w3.org/TR/xml/#charsets
8341
 *
8342
 * @param string $string The string to clean
8343
 * @return string The cleaned string
8344
 */
8345
function cleanXml($string)
8346
{
8347
	global $context;
8348
8349
	$illegal_chars = array(
8350
		// Remove all ASCII control characters except \t, \n, and \r.
8351
		"\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08",
8352
		"\x0B", "\x0C", "\x0E", "\x0F", "\x10", "\x11", "\x12", "\x13", "\x14",
8353
		"\x15", "\x16", "\x17", "\x18", "\x19", "\x1A", "\x1B", "\x1C", "\x1D",
8354
		"\x1E", "\x1F",
8355
		// Remove \xFFFE and \xFFFF
8356
		"\xEF\xBF\xBE", "\xEF\xBF\xBF",
8357
	);
8358
8359
	$string = str_replace($illegal_chars, '', $string);
8360
8361
	// The Unicode surrogate pair code points should never be present in our
8362
	// strings to begin with, but if any snuck in, they need to be removed.
8363
	if (!empty($context['utf8']) && strpos($string, "\xED") !== false)
8364
		$string = preg_replace('/\xED[\xA0-\xBF][\x80-\xBF]/', '', $string);
8365
8366
	return $string;
8367
}
8368
8369
/**
8370
 * Escapes (replaces) characters in strings to make them safe for use in JavaScript
8371
 *
8372
 * @param string $string The string to escape
8373
 * @param bool $as_json If true, escape as double-quoted string. Default false.
8374
 * @return string The escaped string
8375
 */
8376
function JavaScriptEscape($string, $as_json = false)
8377
{
8378
	global $scripturl;
8379
8380
	$q = !empty($as_json) ? '"' : '\'';
8381
8382
	return $q . strtr($string, array(
8383
		"\r" => '',
8384
		"\n" => '\\n',
8385
		"\t" => '\\t',
8386
		'\\' => '\\\\',
8387
		$q => addslashes($q),
8388
		'</' => '<' . $q . ' + ' . $q . '/',
8389
		'<script' => '<scri' . $q . '+' . $q . 'pt',
8390
		'<body>' => '<bo' . $q . '+' . $q . 'dy>',
8391
		'<a href' => '<a hr' . $q . '+' . $q . 'ef',
8392
		$scripturl => $q . ' + smf_scripturl + ' . $q,
8393
	)) . $q;
8394
}
8395
8396
function tokenTxtReplace($stringSubject = '')
8397
{
8398
	global $txt;
8399
8400
	if (empty($stringSubject))
8401
		return '';
8402
8403
	$translatable_tokens = preg_match_all('/{(.*?)}/' , $stringSubject, $matches);
0 ignored issues
show
Unused Code introduced by
The assignment to $translatable_tokens is dead and can be removed.
Loading history...
8404
	$toFind = array();
8405
	$replaceWith = array();
8406
8407
	if (!empty($matches[1]))
8408
		foreach ($matches[1] as $token) {
8409
			$toFind[] = '{' . $token . '}';
8410
			$replaceWith[] = isset($txt[$token]) ? $txt[$token] : $token;
8411
		}
8412
8413
	return str_replace($toFind, $replaceWith, $stringSubject);
8414
}
8415
8416
?>