Passed
Pull Request — release-2.1 (#7367)
by
unknown
06:01
created

template_css()   F

Complexity

Conditions 28
Paths 336

Size

Total Lines 91
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 28
eloc 47
c 0
b 0
f 0
nc 336
nop 0
dl 0
loc 91
rs 1.6333

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.0
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
	$context['current_page'] = $start / $num_per_page;
573
574
	// Define some default page index settings for compatibility with old themes.
575
	// !!! Should this be moved to loadTheme()?
576
	if (!isset($settings['page_index']))
577
		$settings['page_index'] = array(
578
			'extra_before' => '<span class="pages">' . $txt['pages'] . '</span>',
579
			'previous_page' => '<span class="main_icons previous_page"></span>',
580
			'current_page' => '<span class="current_page">%1$d</span> ',
581
			'page' => '<a class="nav_page" href="{URL}">%2$s</a> ',
582
			'expand_pages' => '<span class="expand_pages" onclick="expandPages(this, {LINK}, {FIRST_PAGE}, {LAST_PAGE}, {PER_PAGE});"> ... </span>',
583
			'next_page' => '<span class="main_icons next_page"></span>',
584
			'extra_after' => '',
585
		);
586
587
	$last_page_value = (int) (($max_value - 1) / $num_per_page) * $num_per_page;
588
	$base_link = strtr($settings['page_index']['page'], array('{URL}' => $flexible_start ? $base_url : strtr($base_url, array('%' => '%%')) . ';start=%1$d'));
589
	$pageindex = $settings['page_index']['extra_before'];
590
591
	// Show the "prev page" link. (>prev page< 1 ... 6 7 [8] 9 10 ... 15 next page)
592
	if ($start != 0 && !$start_invalid && $show_prevnext)
593
		$pageindex .= sprintf($base_link, $start - $num_per_page, $settings['page_index']['previous_page']);
594
595
	// Compact pages is off or on?
596
	if (empty($modSettings['compactTopicPagesEnable']))
597
	{
598
		// Show all the pages.
599
		$display_page = 1;
600
		for ($counter = 0; $counter < $max_value; $counter += $num_per_page)
601
			$pageindex .= $start == $counter && !$start_invalid ? sprintf($settings['page_index']['current_page'], $display_page++) : sprintf($base_link, $counter, $display_page++);
602
	}
603
	else
604
	{
605
		// If they didn't enter an odd value, pretend they did.
606
		$page_contiguous = (int) ($modSettings['compactTopicPagesContiguous'] - ($modSettings['compactTopicPagesContiguous'] % 2)) / 2;
607
608
		// Show the first page. (prev page >1< ... 6 7 [8] 9 10 ... 15)
609
		if ($start > $num_per_page * $page_contiguous)
610
			$pageindex .= sprintf($base_link, 0, '1');
611
612
		// Show the ... after the first page.  (prev page 1 >...< 6 7 [8] 9 10 ... 15 next page)
613
		if ($start > $num_per_page * ($page_contiguous + 1))
614
			$pageindex .= strtr($settings['page_index']['expand_pages'], array(
615
				'{LINK}' => JavaScriptEscape($smcFunc['htmlspecialchars']($base_link)),
616
				'{FIRST_PAGE}' => $num_per_page,
617
				'{LAST_PAGE}' => $start - $num_per_page * $page_contiguous,
618
				'{PER_PAGE}' => $num_per_page,
619
			));
620
621
		for ($nCont = -$page_contiguous; $nCont <= $page_contiguous; $nCont++)
622
		{
623
			$tmpStart = $start + $num_per_page * $nCont;
624
			if ($nCont == 0)
625
			{
626
				// Show the current page. (prev page 1 ... 6 7 >[8]< 9 10 ... 15 next page)
627
				if (!$start_invalid)
628
					$pageindex .= sprintf($settings['page_index']['current_page'], $start / $num_per_page + 1);
629
				else
630
					$pageindex .= sprintf($base_link, $start, $start / $num_per_page + 1);
631
			}
632
			// Show the pages before the current one. (prev page 1 ... >6 7< [8] 9 10 ... 15 next page)
633
			// ... or ...
634
			// Show the pages after the current one... (prev page 1 ... 6 7 [8] >9 10< ... 15 next page)
635
			elseif (($nCont < 0 && $start >= $num_per_page * -$nCont) || ($nCont > 0 && $tmpStart <= $last_page_value))
636
				$pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
637
		}
638
639
		// Show the '...' part near the end. (prev page 1 ... 6 7 [8] 9 10 >...< 15 next page)
640
		if ($start + $num_per_page * ($page_contiguous + 1) < $last_page_value)
641
			$pageindex .= strtr($settings['page_index']['expand_pages'], array(
642
				'{LINK}' => JavaScriptEscape($smcFunc['htmlspecialchars']($base_link)),
643
				'{FIRST_PAGE}' => $start + $num_per_page * ($page_contiguous + 1),
644
				'{LAST_PAGE}' => $last_page_value,
645
				'{PER_PAGE}' => $num_per_page,
646
			));
647
648
		// Show the last number in the list. (prev page 1 ... 6 7 [8] 9 10 ... >15<  next page)
649
		if ($start + $num_per_page * $page_contiguous < $last_page_value)
650
			$pageindex .= sprintf($base_link, $last_page_value, $last_page_value / $num_per_page + 1);
651
	}
652
653
	// Show the "next page" link. (prev page 1 ... 6 7 [8] 9 10 ... 15 >next page<)
654
	if ($start != $last_page_value && !$start_invalid && $show_prevnext)
655
		$pageindex .= sprintf($base_link, $start + $num_per_page, $settings['page_index']['next_page']);
656
657
	$pageindex .= $settings['page_index']['extra_after'];
658
659
	return $pageindex;
660
}
661
662
/**
663
 * - Formats a number.
664
 * - uses the format of number_format to decide how to format the number.
665
 *   for example, it might display "1 234,50".
666
 * - caches the formatting data from the setting for optimization.
667
 *
668
 * @param float $number A number
669
 * @param bool|int $override_decimal_count If set, will use the specified number of decimal places. Otherwise it's automatically determined
670
 * @return string A formatted number
671
 */
672
function comma_format($number, $override_decimal_count = false)
673
{
674
	global $txt;
675
	static $thousands_separator = null, $decimal_separator = null, $decimal_count = null;
676
677
	// Cache these values...
678
	if ($decimal_separator === null)
679
	{
680
		// Not set for whatever reason?
681
		if (empty($txt['number_format']) || preg_match('~^1([^\d]*)?234([^\d]*)(0*?)$~', $txt['number_format'], $matches) != 1)
682
			return $number;
683
684
		// Cache these each load...
685
		$thousands_separator = $matches[1];
686
		$decimal_separator = $matches[2];
687
		$decimal_count = strlen($matches[3]);
688
	}
689
690
	// Format the string with our friend, number_format.
691
	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

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

1313
				$substitute_ord = $substitute === '' ? 'none' : mb_ord(/** @scrutinizer ignore-type */ $substitute, $context['character_set']);
Loading history...
1314
1315
				$mb_substitute_character = mb_substitute_character();
1316
				mb_substitute_character($substitute_ord);
1317
1318
				$string = mb_convert_encoding($string, $context['character_set'], $context['character_set']);
1319
1320
				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

1320
				mb_substitute_character(/** @scrutinizer ignore-type */ $mb_substitute_character);
Loading history...
1321
			}
1322
			else
1323
				return false;
1324
		}
1325
	}
1326
1327
	// Fix any weird vertical space characters.
1328
	$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

1328
	$string = normalize_spaces(/** @scrutinizer ignore-type */ $string, true);
Loading history...
1329
1330
	// Deal with unwanted control characters, invisible formatting characters, and other creepy-crawlies.
1331
	if (!empty($context['utf8']))
1332
	{
1333
		require_once($sourcedir . '/Subs-Charset.php');
1334
		$string = utf8_sanitize_invisibles($string, $level, $substitute);
1335
	}
1336
	else
1337
		$string = preg_replace('/[^\P{Cc}\t\r\n]/', $substitute, $string);
1338
1339
	return $string;
1340
}
1341
1342
/**
1343
 * Normalizes space characters and line breaks.
1344
 *
1345
 * @param string $string The string to sanitize.
1346
 * @param bool $vspace If true, replaces all line breaks and vertical space
1347
 *      characters with "\n". Default: true.
1348
 * @param bool $hspace If true, replaces horizontal space characters with a
1349
 *      plain " " character. (Note: tabs are not replaced unless the
1350
 *      'replace_tabs' option is supplied.) Default: false.
1351
 * @param array $options An array of boolean options. Possible values are:
1352
 *      - no_breaks: Vertical spaces are replaced by " " instead of "\n".
1353
 *      - replace_tabs: If true, tabs are are replaced by " " chars.
1354
 *      - collapse_hspace: If true, removes extra horizontal spaces.
1355
 * @return string The sanitized string.
1356
 */
1357
function normalize_spaces($string, $vspace = true, $hspace = false, $options = array())
1358
{
1359
	global $context;
1360
1361
	$string = (string) $string;
1362
	$vspace = !empty($vspace);
1363
	$hspace = !empty($hspace);
1364
1365
	if (!$vspace && !$hspace)
1366
		return $string;
1367
1368
	$options['no_breaks'] = !empty($options['no_breaks']);
1369
	$options['collapse_hspace'] = !empty($options['collapse_hspace']);
1370
	$options['replace_tabs'] = !empty($options['replace_tabs']);
1371
1372
	$patterns = array();
1373
	$replacements = array();
1374
1375
	if ($vspace)
1376
	{
1377
		// \R is like \v, except it handles "\r\n" as a single unit.
1378
		$patterns[] = '/\R/' . ($context['utf8'] ? 'u' : '');
1379
		$replacements[] = $options['no_breaks'] ? ' ' : "\n";
1380
	}
1381
1382
	if ($hspace)
1383
	{
1384
		// Interesting fact: Unicode properties like \p{Zs} work even when not in UTF-8 mode.
1385
		$patterns[] = '/' . ($options['replace_tabs'] ? '\h' : '\p{Zs}') . ($options['collapse_hspace'] ? '+' : '') . '/' . ($context['utf8'] ? 'u' : '');
1386
		$replacements[] = ' ';
1387
	}
1388
1389
	return preg_replace($patterns, $replacements, $string);
1390
}
1391
1392
/**
1393
 * Shorten a subject + internationalization concerns.
1394
 *
1395
 * - shortens a subject so that it is either shorter than length, or that length plus an ellipsis.
1396
 * - respects internationalization characters and entities as one character.
1397
 * - avoids trailing entities.
1398
 * - returns the shortened string.
1399
 *
1400
 * @param string $subject The subject
1401
 * @param int $len How many characters to limit it to
1402
 * @return string The shortened subject - either the entire subject (if it's <= $len) or the subject shortened to $len characters with "..." appended
1403
 */
1404
function shorten_subject($subject, $len)
1405
{
1406
	global $smcFunc;
1407
1408
	// It was already short enough!
1409
	if ($smcFunc['strlen']($subject) <= $len)
1410
		return $subject;
1411
1412
	// Shorten it by the length it was too long, and strip off junk from the end.
1413
	return $smcFunc['substr']($subject, 0, $len) . '...';
1414
}
1415
1416
/**
1417
 * Deprecated function that formerly applied manual offsets to Unix timestamps
1418
 * in order to provide a fake version of time zone support on ancient versions
1419
 * of PHP. It now simply returns an unaltered timestamp.
1420
 *
1421
 * @deprecated since 2.1
1422
 * @param bool $use_user_offset This parameter is deprecated and nonfunctional
1423
 * @param int $timestamp A timestamp (null to use current time)
1424
 * @return int Seconds since the Unix epoch
1425
 */
1426
function forum_time($use_user_offset = true, $timestamp = null)
1427
{
1428
	return !isset($timestamp) ? time() : (int) $timestamp;
1429
}
1430
1431
/**
1432
 * Calculates all the possible permutations (orders) of array.
1433
 * should not be called on huge arrays (bigger than like 10 elements.)
1434
 * returns an array containing each permutation.
1435
 *
1436
 * @deprecated since 2.1
1437
 * @param array $array An array
1438
 * @return array An array containing each permutation
1439
 */
1440
function permute($array)
1441
{
1442
	$orders = array($array);
1443
1444
	$n = count($array);
1445
	$p = range(0, $n);
1446
	for ($i = 1; $i < $n; null)
1447
	{
1448
		$p[$i]--;
1449
		$j = $i % 2 != 0 ? $p[$i] : 0;
1450
1451
		$temp = $array[$i];
1452
		$array[$i] = $array[$j];
1453
		$array[$j] = $temp;
1454
1455
		for ($i = 1; $p[$i] == 0; $i++)
1456
			$p[$i] = 1;
1457
1458
		$orders[] = $array;
1459
	}
1460
1461
	return $orders;
1462
}
1463
1464
/**
1465
 * Return an array with allowed bbc tags for signatures, that can be passed to parse_bbc().
1466
 *
1467
 * @return array An array containing allowed tags for signatures, or an empty array if all tags are allowed.
1468
 */
1469
function get_signature_allowed_bbc_tags()
1470
{
1471
	global $modSettings;
1472
1473
	list ($sig_limits, $sig_bbc) = explode(':', $modSettings['signature_settings']);
1474
	if (empty($sig_bbc))
1475
		return array();
1476
	$disabledTags = explode(',', $sig_bbc);
1477
1478
	// Get all available bbc tags
1479
	$temp = parse_bbc(false);
1480
	$allowedTags = array();
1481
	foreach ($temp as $tag)
0 ignored issues
show
Bug introduced by
The expression $temp of type string is not traversable.
Loading history...
1482
		if (!in_array($tag['tag'], $disabledTags))
1483
			$allowedTags[] = $tag['tag'];
1484
1485
	$allowedTags = array_unique($allowedTags);
1486
	if (empty($allowedTags))
1487
		// An empty array means that all bbc tags are allowed. So if all tags are disabled we need to add a dummy tag.
1488
		$allowedTags[] = 'nonexisting';
1489
1490
	return $allowedTags;
1491
}
1492
1493
/**
1494
 * Parse bulletin board code in a string, as well as smileys optionally.
1495
 *
1496
 * - only parses bbc tags which are not disabled in disabledBBC.
1497
 * - handles basic HTML, if enablePostHTML is on.
1498
 * - caches the from/to replace regular expressions so as not to reload them every time a string is parsed.
1499
 * - only parses smileys if smileys is true.
1500
 * - does nothing if the enableBBC setting is off.
1501
 * - uses the cache_id as a unique identifier to facilitate any caching it may do.
1502
 * - returns the modified message.
1503
 *
1504
 * @param string|bool $message The message.
1505
 *		When a empty string, nothing is done.
1506
 *		When false we provide a list of BBC codes available.
1507
 *		When a string, the message is parsed and bbc handled.
1508
 * @param bool $smileys Whether to parse smileys as well
1509
 * @param string $cache_id The cache ID
1510
 * @param array $parse_tags If set, only parses these tags rather than all of them
1511
 * @return string The parsed message
1512
 */
1513
function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = array())
1514
{
1515
	global $smcFunc, $txt, $scripturl, $context, $modSettings, $user_info, $sourcedir, $cache_enable;
1516
	static $bbc_lang_locales = array(), $itemcodes = array(), $no_autolink_tags = array();
1517
	static $disabled, $alltags_regex = '', $param_regexes = array(), $url_regex = '';
1518
	$returncodes = ($message === false ? true : false);
1519
1520
	// Don't waste cycles
1521
	if ($message === '' && !$returncodes)
1522
		return '';
1523
1524
	// Just in case it wasn't determined yet whether UTF-8 is enabled.
1525
	if (!isset($context['utf8']))
1526
		$context['utf8'] = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8';
1527
1528
	// Clean up any cut/paste issues we may have
1529
	$message = sanitizeMSCutPaste($message);
1530
1531
	// If the load average is too high, don't parse the BBC.
1532
	if (!empty($context['load_average']) && !empty($modSettings['bbc']) && $context['load_average'] >= $modSettings['bbc'] && !$returncodes)
1533
	{
1534
		$context['disabled_parse_bbc'] = true;
1535
		return $message;
1536
	}
1537
1538
	if ($smileys !== null && ($smileys == '1' || $smileys == '0'))
1539
		$smileys = (bool) $smileys;
1540
1541
	if (empty($modSettings['enableBBC']) && !$returncodes)
1542
	{
1543
		if ($smileys === true)
1544
			parsesmileys($message);
1545
		return $message;
1546
	}
1547
1548
	// If we already have a version of the BBCodes for the current language, use that. Otherwise, make one.
1549
	if (!empty($bbc_lang_locales[$txt['lang_locale']]))
1550
		$bbc_codes = $bbc_lang_locales[$txt['lang_locale']];
1551
	else
1552
		$bbc_codes = array();
1553
1554
	// If we are not doing every tag then we don't cache this run.
1555
	if (!empty($parse_tags))
1556
		$bbc_codes = array();
1557
1558
	// Ensure $modSettings['tld_regex'] contains a valid regex for the autolinker
1559
	if (!empty($modSettings['autoLinkUrls']))
1560
		set_tld_regex();
1561
	// Allow mods access before entering the main parse_bbc loop
1562
	call_integration_hook('integrate_pre_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
1563
1564
	// Sift out the bbc for a performance improvement.
1565
	if (empty($bbc_codes) || $returncodes || !empty($parse_tags))
1566
	{
1567
		if (!empty($modSettings['disabledBBC']))
1568
		{
1569
			$disabled = array();
1570
1571
			$temp = explode(',', strtolower($modSettings['disabledBBC']));
1572
1573
			foreach ($temp as $tag)
1574
				$disabled[trim($tag)] = true;
1575
1576
			if (in_array('color', $disabled))
1577
				$disabled = array_merge($disabled, array(
1578
					'black' => true,
1579
					'white' => true,
1580
					'red' => true,
1581
					'green' => true,
1582
					'blue' => true,
1583
					)
1584
				);
1585
		}
1586
1587
		if (!empty($parse_tags))
1588
		{
1589
			if (!in_array('email', $parse_tags))
1590
				$disabled['email'] = true;
1591
			if (!in_array('url', $parse_tags))
1592
				$disabled['url'] = true;
1593
			if (!in_array('iurl', $parse_tags))
1594
				$disabled['iurl'] = true;
1595
		}
1596
1597
		// The YouTube bbc needs this for its origin parameter
1598
		$scripturl_parts = parse_iri($scripturl);
1599
		$hosturl = $scripturl_parts['scheme'] . '://' . $scripturl_parts['host'];
1600
1601
		/* The following bbc are formatted as an array, with keys as follows:
1602
1603
			tag: the tag's name - should be lowercase!
1604
1605
			type: one of...
1606
				- (missing): [tag]parsed content[/tag]
1607
				- unparsed_equals: [tag=xyz]parsed content[/tag]
1608
				- parsed_equals: [tag=parsed data]parsed content[/tag]
1609
				- unparsed_content: [tag]unparsed content[/tag]
1610
				- closed: [tag], [tag/], [tag /]
1611
				- unparsed_commas: [tag=1,2,3]parsed content[/tag]
1612
				- unparsed_commas_content: [tag=1,2,3]unparsed content[/tag]
1613
				- unparsed_equals_content: [tag=...]unparsed content[/tag]
1614
1615
			parameters: an optional array of parameters, for the form
1616
			  [tag abc=123]content[/tag].  The array is an associative array
1617
			  where the keys are the parameter names, and the values are an
1618
			  array which may contain the following:
1619
				- match: a regular expression to validate and match the value.
1620
				- quoted: true if the value should be quoted.
1621
				- validate: callback to evaluate on the data, which is $data.
1622
				- value: a string in which to replace $1 with the data.
1623
					Either value or validate may be used, not both.
1624
				- optional: true if the parameter is optional.
1625
				- default: a default value for missing optional parameters.
1626
1627
			test: a regular expression to test immediately after the tag's
1628
			  '=', ' ' or ']'.  Typically, should have a \] at the end.
1629
			  Optional.
1630
1631
			content: only available for unparsed_content, closed,
1632
			  unparsed_commas_content, and unparsed_equals_content.
1633
			  $1 is replaced with the content of the tag.  Parameters
1634
			  are replaced in the form {param}.  For unparsed_commas_content,
1635
			  $2, $3, ..., $n are replaced.
1636
1637
			before: only when content is not used, to go before any
1638
			  content.  For unparsed_equals, $1 is replaced with the value.
1639
			  For unparsed_commas, $1, $2, ..., $n are replaced.
1640
1641
			after: similar to before in every way, except that it is used
1642
			  when the tag is closed.
1643
1644
			disabled_content: used in place of content when the tag is
1645
			  disabled.  For closed, default is '', otherwise it is '$1' if
1646
			  block_level is false, '<div>$1</div>' elsewise.
1647
1648
			disabled_before: used in place of before when disabled.  Defaults
1649
			  to '<div>' if block_level, '' if not.
1650
1651
			disabled_after: used in place of after when disabled.  Defaults
1652
			  to '</div>' if block_level, '' if not.
1653
1654
			block_level: set to true the tag is a "block level" tag, similar
1655
			  to HTML.  Block level tags cannot be nested inside tags that are
1656
			  not block level, and will not be implicitly closed as easily.
1657
			  One break following a block level tag may also be removed.
1658
1659
			trim: if set, and 'inside' whitespace after the begin tag will be
1660
			  removed.  If set to 'outside', whitespace after the end tag will
1661
			  meet the same fate.
1662
1663
			validate: except when type is missing or 'closed', a callback to
1664
			  validate the data as $data.  Depending on the tag's type, $data
1665
			  may be a string or an array of strings (corresponding to the
1666
			  replacement.)
1667
1668
			quoted: when type is 'unparsed_equals' or 'parsed_equals' only,
1669
			  may be not set, 'optional', or 'required' corresponding to if
1670
			  the content may be quoted.  This allows the parser to read
1671
			  [tag="abc]def[esdf]"] properly.
1672
1673
			require_parents: an array of tag names, or not set.  If set, the
1674
			  enclosing tag *must* be one of the listed tags, or parsing won't
1675
			  occur.
1676
1677
			require_children: similar to require_parents, if set children
1678
			  won't be parsed if they are not in the list.
1679
1680
			disallow_children: similar to, but very different from,
1681
			  require_children, if it is set the listed tags will not be
1682
			  parsed inside the tag.
1683
1684
			parsed_tags_allowed: an array restricting what BBC can be in the
1685
			  parsed_equals parameter, if desired.
1686
		*/
1687
1688
		$codes = array(
1689
			array(
1690
				'tag' => 'abbr',
1691
				'type' => 'unparsed_equals',
1692
				'before' => '<abbr title="$1">',
1693
				'after' => '</abbr>',
1694
				'quoted' => 'optional',
1695
				'disabled_after' => ' ($1)',
1696
			),
1697
			// Legacy (and just an alias for [abbr] even when enabled)
1698
			array(
1699
				'tag' => 'acronym',
1700
				'type' => 'unparsed_equals',
1701
				'before' => '<abbr title="$1">',
1702
				'after' => '</abbr>',
1703
				'quoted' => 'optional',
1704
				'disabled_after' => ' ($1)',
1705
			),
1706
			array(
1707
				'tag' => 'anchor',
1708
				'type' => 'unparsed_equals',
1709
				'test' => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]',
1710
				'before' => '<span id="post_$1">',
1711
				'after' => '</span>',
1712
			),
1713
			array(
1714
				'tag' => 'attach',
1715
				'type' => 'unparsed_content',
1716
				'parameters' => array(
1717
					'id' => array('match' => '(\d+)'),
1718
					'alt' => array('optional' => true),
1719
					'width' => array('optional' => true, 'match' => '(\d+)'),
1720
					'height' => array('optional' => true, 'match' => '(\d+)'),
1721
					'display' => array('optional' => true, 'match' => '(link|embed)'),
1722
				),
1723
				'content' => '$1',
1724
				'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...
1725
				{
1726
					$returnContext = '';
1727
1728
					// BBC or the entire attachments feature is disabled
1729
					if (empty($modSettings['attachmentEnable']) || !empty($disabled['attach']))
1730
						return $data;
1731
1732
					// Save the attach ID.
1733
					$attachID = $params['{id}'];
1734
1735
					// Kinda need this.
1736
					require_once($sourcedir . '/Subs-Attachments.php');
1737
1738
					$currentAttachment = parseAttachBBC($attachID);
1739
1740
					// parseAttachBBC will return a string ($txt key) rather than dying with a fatal_error. Up to you to decide what to do.
1741
					if (is_string($currentAttachment))
1742
						return $data = !empty($txt[$currentAttachment]) ? $txt[$currentAttachment] : $currentAttachment;
1743
1744
					// We need a display mode.
1745
					if (empty($params['{display}']))
1746
					{
1747
						// Images, video, and audio are embedded by default.
1748
						if (!empty($currentAttachment['is_image']) || strpos($currentAttachment['mime_type'], 'video/') === 0 || strpos($currentAttachment['mime_type'], 'audio/') === 0)
1749
							$params['{display}'] = 'embed';
1750
						// Anything else shows a link by default.
1751
						else
1752
							$params['{display}'] = 'link';
1753
					}
1754
1755
					// Embedded file.
1756
					if ($params['{display}'] == 'embed')
1757
					{
1758
						$alt = ' alt="' . (!empty($params['{alt}']) ? $params['{alt}'] : $currentAttachment['name']) . '"';
1759
						$title = !empty($data) ? ' title="' . $smcFunc['htmlspecialchars']($data) . '"' : '';
1760
1761
						// Image.
1762
						if (!empty($currentAttachment['is_image']))
1763
						{
1764
							if (empty($params['{width}']) && empty($params['{height}']))
1765
								$returnContext .= '<img src="' . $currentAttachment['href'] . '"' . $alt . $title . ' class="bbc_img">';
1766
							else
1767
							{
1768
								$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"': '';
1769
								$height = !empty($params['{height}']) ? 'height="' . $params['{height}'] . '"' : '';
1770
								$returnContext .= '<img src="' . $currentAttachment['href'] . ';image"' . $alt . $title . $width . $height . ' class="bbc_img resized"/>';
1771
							}
1772
						}
1773
						// Video.
1774
						elseif (strpos($currentAttachment['mime_type'], 'video/') === 0)
1775
						{
1776
							$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"' : '';
1777
							$height = !empty($params['{height}']) ? ' height="' . $params['{height}'] . '"' : '';
1778
1779
							$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>' : '');
1780
						}
1781
						// Audio.
1782
						elseif (strpos($currentAttachment['mime_type'], 'audio/') === 0)
1783
						{
1784
							$width = 'max-width:100%; width: ' . (!empty($params['{width}']) ? $params['{width}'] : '400') . 'px;';
1785
							$height = !empty($params['{height}']) ? 'height: ' . $params['{height}'] . 'px;' : '';
1786
1787
							$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>';
1788
						}
1789
						// Anything else.
1790
						else
1791
						{
1792
							$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"' : '';
1793
							$height = !empty($params['{height}']) ? ' height="' . $params['{height}'] . '"' : '';
1794
1795
							$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>';
1796
						}
1797
					}
1798
1799
					// No image. Show a link.
1800
					else
1801
						$returnContext .= '<a href="' . $currentAttachment['href'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . '</a>';
1802
1803
					// Use this hook to adjust the HTML output of the attach BBCode.
1804
					// If you want to work with the attachment data itself, use one of these:
1805
					// - integrate_pre_parseAttachBBC
1806
					// - integrate_post_parseAttachBBC
1807
					call_integration_hook('integrate_attach_bbc_validate', array(&$returnContext, $currentAttachment, $tag, $data, $disabled, $params));
1808
1809
					// Gotta append what we just did.
1810
					$data = $returnContext;
1811
				},
1812
			),
1813
			array(
1814
				'tag' => 'b',
1815
				'before' => '<b>',
1816
				'after' => '</b>',
1817
			),
1818
			// Legacy (equivalent to [ltr] or [rtl])
1819
			array(
1820
				'tag' => 'bdo',
1821
				'type' => 'unparsed_equals',
1822
				'before' => '<bdo dir="$1">',
1823
				'after' => '</bdo>',
1824
				'test' => '(rtl|ltr)\]',
1825
				'block_level' => true,
1826
			),
1827
			// Legacy (alias of [color=black])
1828
			array(
1829
				'tag' => 'black',
1830
				'before' => '<span style="color: black;" class="bbc_color">',
1831
				'after' => '</span>',
1832
			),
1833
			// Legacy (alias of [color=blue])
1834
			array(
1835
				'tag' => 'blue',
1836
				'before' => '<span style="color: blue;" class="bbc_color">',
1837
				'after' => '</span>',
1838
			),
1839
			array(
1840
				'tag' => 'br',
1841
				'type' => 'closed',
1842
				'content' => '<br>',
1843
			),
1844
			array(
1845
				'tag' => 'center',
1846
				'before' => '<div class="centertext">',
1847
				'after' => '</div>',
1848
				'block_level' => true,
1849
			),
1850
			array(
1851
				'tag' => 'code',
1852
				'type' => 'unparsed_content',
1853
				'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>',
1854
				// @todo Maybe this can be simplified?
1855
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1856
				{
1857
					if (!isset($disabled['code']))
1858
					{
1859
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
1860
1861
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
1862
						{
1863
							// Do PHP code coloring?
1864
							if ($php_parts[$php_i] != '&lt;?php')
1865
								continue;
1866
1867
							$php_string = '';
1868
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1869
							{
1870
								$php_string .= $php_parts[$php_i];
1871
								$php_parts[$php_i++] = '';
1872
							}
1873
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1874
						}
1875
1876
						// Fix the PHP code stuff...
1877
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1878
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1879
1880
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1881
						if (!empty($context['browser']['is_opera']))
1882
							$data .= '&nbsp;';
1883
					}
1884
				},
1885
				'block_level' => true,
1886
			),
1887
			array(
1888
				'tag' => 'code',
1889
				'type' => 'unparsed_equals_content',
1890
				'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>',
1891
				// @todo Maybe this can be simplified?
1892
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1893
				{
1894
					if (!isset($disabled['code']))
1895
					{
1896
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1897
1898
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
1899
						{
1900
							// Do PHP code coloring?
1901
							if ($php_parts[$php_i] != '&lt;?php')
1902
								continue;
1903
1904
							$php_string = '';
1905
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1906
							{
1907
								$php_string .= $php_parts[$php_i];
1908
								$php_parts[$php_i++] = '';
1909
							}
1910
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1911
						}
1912
1913
						// Fix the PHP code stuff...
1914
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1915
						$data[0] = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data[0]);
1916
1917
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1918
						if (!empty($context['browser']['is_opera']))
1919
							$data[0] .= '&nbsp;';
1920
					}
1921
				},
1922
				'block_level' => true,
1923
			),
1924
			array(
1925
				'tag' => 'color',
1926
				'type' => 'unparsed_equals',
1927
				'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]?)\))\]',
1928
				'before' => '<span style="color: $1;" class="bbc_color">',
1929
				'after' => '</span>',
1930
			),
1931
			array(
1932
				'tag' => 'email',
1933
				'type' => 'unparsed_content',
1934
				'content' => '<a href="mailto:$1" class="bbc_email">$1</a>',
1935
				// @todo Should this respect guest_hideContacts?
1936
				'validate' => function(&$tag, &$data, $disabled)
1937
				{
1938
					$data = strtr($data, array('<br>' => ''));
1939
				},
1940
			),
1941
			array(
1942
				'tag' => 'email',
1943
				'type' => 'unparsed_equals',
1944
				'before' => '<a href="mailto:$1" class="bbc_email">',
1945
				'after' => '</a>',
1946
				// @todo Should this respect guest_hideContacts?
1947
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1948
				'disabled_after' => ' ($1)',
1949
			),
1950
			// Legacy (and just a link even when not disabled)
1951
			array(
1952
				'tag' => 'flash',
1953
				'type' => 'unparsed_commas_content',
1954
				'test' => '\d+,\d+\]',
1955
				'content' => '<a href="$1" target="_blank" rel="noopener">$1</a>',
1956
				'validate' => function (&$tag, &$data, $disabled)
1957
				{
1958
					$data[0] = normalize_iri($data[0]);
1959
1960
					$scheme = parse_iri($data[0], PHP_URL_SCHEME);
1961
					if (empty($scheme))
1962
						$data[0] = '//' . ltrim($data[0], ':/');
1963
1964
					$ascii_url = iri_to_url($data[0]);
1965
					if ($ascii_url !== $data[0])
1966
						$tag['content'] = str_replace('href="$1"', 'href="' . $ascii_url . '"', $tag['content']);
1967
				},
1968
			),
1969
			array(
1970
				'tag' => 'float',
1971
				'type' => 'unparsed_equals',
1972
				'test' => '(left|right)(\s+max=\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)?\]',
1973
				'before' => '<div $1>',
1974
				'after' => '</div>',
1975
				'validate' => function(&$tag, &$data, $disabled)
1976
				{
1977
					$class = 'class="bbc_float float' . (strpos($data, 'left') === 0 ? 'left' : 'right') . '"';
1978
1979
					if (preg_match('~\bmax=(\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)~', $data, $matches))
1980
						$css = ' style="max-width:' . $matches[1] . (is_numeric($matches[1]) ? 'px' : '') . '"';
1981
					else
1982
						$css = '';
1983
1984
					$data = $class . $css;
1985
				},
1986
				'trim' => 'outside',
1987
				'block_level' => true,
1988
			),
1989
			// Legacy (alias of [url] with an FTP URL)
1990
			array(
1991
				'tag' => 'ftp',
1992
				'type' => 'unparsed_content',
1993
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
1994
				'validate' => function(&$tag, &$data, $disabled)
1995
				{
1996
					$data = normalize_iri(strtr($data, array('<br>' => '')));
1997
1998
					$scheme = parse_iri($data, PHP_URL_SCHEME);
1999
					if (empty($scheme))
2000
						$data = 'ftp://' . ltrim($data, ':/');
2001
2002
					$ascii_url = iri_to_url($data);
2003
					if ($ascii_url !== $data)
2004
						$tag['content'] = str_replace('href="$1"', 'href="' . $ascii_url . '"', $tag['content']);
2005
				},
2006
			),
2007
			// Legacy (alias of [url] with an FTP URL)
2008
			array(
2009
				'tag' => 'ftp',
2010
				'type' => 'unparsed_equals',
2011
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
2012
				'after' => '</a>',
2013
				'validate' => function(&$tag, &$data, $disabled)
2014
				{
2015
					$data = iri_to_url($data);
2016
2017
					$scheme = parse_iri($data, PHP_URL_SCHEME);
2018
					if (empty($scheme))
2019
						$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

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

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

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

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

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

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

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

3083
								$url_regex .= '(?<' . $name . '>' . /** @scrutinizer ignore-type */ $subroutine . ')';
Loading history...
3084
3085
							$url_regex .= ')';
3086
						}
3087
3088
						$tmp_data = preg_replace_callback(
3089
							'~' . $url_regex . '~i' . ($context['utf8'] ? 'u' : ''),
3090
							function($matches) use ($schemes)
3091
							{
3092
								$url = array_shift($matches);
3093
3094
								// If this isn't a clean URL, bail out
3095
								if ($url != sanitize_iri($url))
3096
									return $url;
3097
3098
								// Ensure the host name is in its canonical form.
3099
								$url = normalize_iri($url);
3100
3101
								$parsedurl = parse_iri($url);
3102
3103
								if (!isset($parsedurl['scheme']))
3104
									$parsedurl['scheme'] = '';
3105
3106
								if ($parsedurl['scheme'] == 'mailto')
3107
								{
3108
									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...
3109
										return $url;
3110
3111
									// Is this version of PHP capable of validating this email address?
3112
									$can_validate = defined('FILTER_FLAG_EMAIL_UNICODE') || strlen($parsedurl['path']) == strspn(strtolower($parsedurl['path']), 'abcdefghijklmnopqrstuvwxyz0123456789!#$%&\'*+-/=?^_`{|}~.@');
3113
3114
									$flags = defined('FILTER_FLAG_EMAIL_UNICODE') ? FILTER_FLAG_EMAIL_UNICODE : null;
3115
3116
									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

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

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

4138
					ob_start(/** @scrutinizer ignore-type */ $call);
Loading history...
4139
			}
4140
4141
		// Display the screen in the logical order.
4142
		template_header();
4143
		$header_done = true;
4144
	}
4145
	if ($do_footer)
4146
	{
4147
		loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main');
4148
4149
		// Anything special to put out?
4150
		if (!empty($context['insert_after_template']) && !isset($_REQUEST['xml']))
4151
			echo $context['insert_after_template'];
4152
4153
		// Just so we don't get caught in an endless loop of errors from the footer...
4154
		if (!$footer_done)
4155
		{
4156
			$footer_done = true;
4157
			template_footer();
4158
4159
			// (since this is just debugging... it's okay that it's after </html>.)
4160
			if (!isset($_REQUEST['xml']))
4161
				displayDebug();
4162
		}
4163
	}
4164
4165
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
4166
	if ($should_log)
4167
		$_SESSION['old_url'] = $_SERVER['REQUEST_URL'];
4168
4169
	// For session check verification.... don't switch browsers...
4170
	$_SESSION['USER_AGENT'] = empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT'];
4171
4172
	// Hand off the output to the portal, etc. we're integrated with.
4173
	call_integration_hook('integrate_exit', array($do_footer));
4174
4175
	// Don't exit if we're coming from index.php; that will pass through normally.
4176
	if (!$from_index)
4177
		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...
4178
}
4179
4180
/**
4181
 * Get the size of a specified image with better error handling.
4182
 *
4183
 * @todo see if it's better in Subs-Graphics, but one step at the time.
4184
 * Uses getimagesize() to determine the size of a file.
4185
 * Attempts to connect to the server first so it won't time out.
4186
 *
4187
 * @param string $url The URL of the image
4188
 * @return array|false The image size as array (width, height), or false on failure
4189
 */
4190
function url_image_size($url)
4191
{
4192
	global $sourcedir;
4193
4194
	// Make sure it is a proper URL.
4195
	$url = str_replace(' ', '%20', $url);
4196
4197
	// Can we pull this from the cache... please please?
4198
	if (($temp = cache_get_data('url_image_size-' . md5($url), 240)) !== null)
4199
		return $temp;
4200
	$t = microtime(true);
4201
4202
	// Get the host to pester...
4203
	preg_match('~^\w+://(.+?)/(.*)$~', $url, $match);
4204
4205
	// Can't figure it out, just try the image size.
4206
	if ($url == '' || $url == 'http://' || $url == 'https://')
4207
	{
4208
		return false;
4209
	}
4210
	elseif (!isset($match[1]))
4211
	{
4212
		$size = @getimagesize($url);
4213
	}
4214
	else
4215
	{
4216
		// Try to connect to the server... give it half a second.
4217
		$temp = 0;
4218
		$fp = @fsockopen($match[1], 80, $temp, $temp, 0.5);
4219
4220
		// Successful?  Continue...
4221
		if ($fp != false)
4222
		{
4223
			// Send the HEAD request (since we don't have to worry about chunked, HTTP/1.1 is fine here.)
4224
			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");
4225
4226
			// Read in the HTTP/1.1 or whatever.
4227
			$test = substr(fgets($fp, 11), -1);
4228
			fclose($fp);
4229
4230
			// See if it returned a 404/403 or something.
4231
			if ($test < 4)
4232
			{
4233
				$size = @getimagesize($url);
4234
4235
				// This probably means allow_url_fopen is off, let's try GD.
4236
				if ($size === false && function_exists('imagecreatefromstring'))
4237
				{
4238
					// It's going to hate us for doing this, but another request...
4239
					$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

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

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

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

5231
		if (strpos(/** @scrutinizer ignore-type */ $test, 'not found') !== false)
Loading history...
5232
			$host = '';
5233
		// Invalid server option?
5234
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
5235
			updateSettings(array('host_to_dis' => 1));
5236
		// Maybe it found something, after all?
5237
		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

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

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

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

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

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

6298
			$temp_file = tempnam($cachedir, md5(/** @scrutinizer ignore-type */ $data));
Loading history...
6299
			file_put_contents($temp_file, $data);
6300
			$is_path = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $is_path is dead and can be removed.
Loading history...
6301
			$data = $temp_file;
6302
		}
6303
6304
		$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

6304
		$imagetype = @exif_imagetype(/** @scrutinizer ignore-type */ $data);
Loading history...
6305
6306
		if (isset($temp_file))
6307
			unlink($temp_file);
6308
6309
		// Unfortunately, this workaround only works for image files.
6310
		if ($imagetype !== false)
6311
			$mime_type = image_type_to_mime_type($imagetype);
6312
	}
6313
6314
	return $mime_type;
6315
}
6316
6317
/**
6318
 * Checks whether a file or data has the expected MIME type.
6319
 *
6320
 * @param string $data The data to check, or the path or URL of a file to check.
6321
 * @param string $type_pattern A regex pattern to match the acceptable MIME types.
6322
 * @param string $is_path If true, $data is a path or URL to a file.
6323
 * @return int 1 if the detected MIME type matches the pattern, 0 if it doesn't, or 2 if we can't check.
6324
 */
6325
function check_mime_type($data, $type_pattern, $is_path = false)
6326
{
6327
	// Get the MIME type.
6328
	$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

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

6455
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
6456
6457
	// remove left to right / right to left overrides
6458
	if ($num === 0x202D || $num === 0x202E)
6459
		return '';
6460
6461
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
6462
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
6463
		return '&#' . $num . ';';
6464
6465
	if (empty($context['utf8']))
6466
	{
6467
		// no control characters
6468
		if ($num < 0x20)
6469
			return '';
6470
		// text is text
6471
		elseif ($num < 0x80)
6472
			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

6472
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
6473
		// all others get html-ised
6474
		else
6475
			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

6475
			return '&#' . /** @scrutinizer ignore-type */ $matches[2] . ';';
Loading history...
6476
	}
6477
	else
6478
	{
6479
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
6480
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
6481
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
6482
			return '';
6483
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
6484
		elseif ($num < 0x80)
6485
			return chr($num);
6486
		// <0x800 (2048)
6487
		elseif ($num < 0x800)
6488
			return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
6489
		// < 0x10000 (65536)
6490
		elseif ($num < 0x10000)
6491
			return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6492
		// <= 0x10FFFF (1114111)
6493
		else
6494
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6495
	}
6496
}
6497
6498
/**
6499
 * Converts html entities to utf8 equivalents
6500
 *
6501
 * Callback function for preg_replace_callback
6502
 * Uses capture group 1 in the supplied array
6503
 * Does basic checks to keep characters inside a viewable range.
6504
 *
6505
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
6506
 * @return string The fixed string
6507
 */
6508
function fixchar__callback($matches)
6509
{
6510
	if (!isset($matches[1]))
6511
		return '';
6512
6513
	$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

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

6521
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
6522
	// <0x800 (2048)
6523
	elseif ($num < 0x800)
6524
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
6525
	// < 0x10000 (65536)
6526
	elseif ($num < 0x10000)
6527
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6528
	// <= 0x10FFFF (1114111)
6529
	else
6530
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6531
}
6532
6533
/**
6534
 * Strips out invalid html entities, replaces others with html style &#123; codes
6535
 *
6536
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
6537
 * strpos, strlen, substr etc
6538
 *
6539
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
6540
 * @return string The fixed string
6541
 */
6542
function entity_fix__callback($matches)
6543
{
6544
	if (!isset($matches[2]))
6545
		return '';
6546
6547
	$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

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

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

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

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

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

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

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

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

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

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

}

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

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

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

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

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

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

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

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

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

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

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

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

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