Passed
Push — release-2.1 ( 12d126...77eee1 )
by Mathias
07:47 queued 16s
created

smf_strftime()   F

Complexity

Conditions 43
Paths 4744

Size

Total Lines 311
Code Lines 189

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 43
eloc 189
c 1
b 0
f 0
nop 3
dl 0
loc 311
rs 0
nc 4744

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file has all the main functions in it that relate to, well, everything.
5
 *
6
 * Simple Machines Forum (SMF)
7
 *
8
 * @package SMF
9
 * @author Simple Machines https://www.simplemachines.org
10
 * @copyright 2021 Simple Machines and individual contributors
11
 * @license https://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1 RC4
14
 */
15
16
if (!defined('SMF'))
17
	die('No direct access...');
18
19
/**
20
 * Update some basic statistics.
21
 *
22
 * 'member' statistic updates the latest member, the total member
23
 *  count, and the number of unapproved members.
24
 * 'member' also only counts approved members when approval is on, but
25
 *  is much more efficient with it off.
26
 *
27
 * 'message' changes the total number of messages, and the
28
 *  highest message id by id_msg - which can be parameters 1 and 2,
29
 *  respectively.
30
 *
31
 * 'topic' updates the total number of topics, or if parameter1 is true
32
 *  simply increments them.
33
 *
34
 * 'subject' updates the log_search_subjects in the event of a topic being
35
 *  moved, removed or split.  parameter1 is the topicid, parameter2 is the new subject
36
 *
37
 * 'postgroups' case updates those members who match condition's
38
 *  post-based membergroups in the database (restricted by parameter1).
39
 *
40
 * @param string $type Stat type - can be 'member', 'message', 'topic', 'subject' or 'postgroups'
41
 * @param mixed $parameter1 A parameter for updating the stats
42
 * @param mixed $parameter2 A 2nd parameter for updating the stats
43
 */
44
function updateStats($type, $parameter1 = null, $parameter2 = null)
45
{
46
	global $modSettings, $smcFunc, $txt;
47
48
	switch ($type)
49
	{
50
		case 'member':
51
			$changes = array(
52
				'memberlist_updated' => time(),
53
			);
54
55
			// #1 latest member ID, #2 the real name for a new registration.
56
			if (is_numeric($parameter1))
57
			{
58
				$changes['latestMember'] = $parameter1;
59
				$changes['latestRealName'] = $parameter2;
60
61
				updateSettings(array('totalMembers' => true), true);
62
			}
63
64
			// We need to calculate the totals.
65
			else
66
			{
67
				// Update the latest activated member (highest id_member) and count.
68
				$result = $smcFunc['db_query']('', '
69
					SELECT COUNT(*), MAX(id_member)
70
					FROM {db_prefix}members
71
					WHERE is_activated = {int:is_activated}',
72
					array(
73
						'is_activated' => 1,
74
					)
75
				);
76
				list ($changes['totalMembers'], $changes['latestMember']) = $smcFunc['db_fetch_row']($result);
77
				$smcFunc['db_free_result']($result);
78
79
				// Get the latest activated member's display name.
80
				$result = $smcFunc['db_query']('', '
81
					SELECT real_name
82
					FROM {db_prefix}members
83
					WHERE id_member = {int:id_member}
84
					LIMIT 1',
85
					array(
86
						'id_member' => (int) $changes['latestMember'],
87
					)
88
				);
89
				list ($changes['latestRealName']) = $smcFunc['db_fetch_row']($result);
90
				$smcFunc['db_free_result']($result);
91
92
				// Update the amount of members awaiting approval
93
				$result = $smcFunc['db_query']('', '
94
					SELECT COUNT(*)
95
					FROM {db_prefix}members
96
					WHERE is_activated IN ({array_int:activation_status})',
97
					array(
98
						'activation_status' => array(3, 4, 5),
99
					)
100
				);
101
102
				list ($changes['unapprovedMembers']) = $smcFunc['db_fetch_row']($result);
103
				$smcFunc['db_free_result']($result);
104
			}
105
			updateSettings($changes);
106
			break;
107
108
		case 'message':
109
			if ($parameter1 === true && $parameter2 !== null)
110
				updateSettings(array('totalMessages' => true, 'maxMsgID' => $parameter2), true);
111
			else
112
			{
113
				// SUM and MAX on a smaller table is better for InnoDB tables.
114
				$result = $smcFunc['db_query']('', '
115
					SELECT SUM(num_posts + unapproved_posts) AS total_messages, MAX(id_last_msg) AS max_msg_id
116
					FROM {db_prefix}boards
117
					WHERE redirect = {string:blank_redirect}' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
118
						AND id_board != {int:recycle_board}' : ''),
119
					array(
120
						'recycle_board' => isset($modSettings['recycle_board']) ? $modSettings['recycle_board'] : 0,
121
						'blank_redirect' => '',
122
					)
123
				);
124
				$row = $smcFunc['db_fetch_assoc']($result);
125
				$smcFunc['db_free_result']($result);
126
127
				updateSettings(array(
128
					'totalMessages' => $row['total_messages'] === null ? 0 : $row['total_messages'],
129
					'maxMsgID' => $row['max_msg_id'] === null ? 0 : $row['max_msg_id']
130
				));
131
			}
132
			break;
133
134
		case 'subject':
135
			// Remove the previous subject (if any).
136
			$smcFunc['db_query']('', '
137
				DELETE FROM {db_prefix}log_search_subjects
138
				WHERE id_topic = {int:id_topic}',
139
				array(
140
					'id_topic' => (int) $parameter1,
141
				)
142
			);
143
144
			// Insert the new subject.
145
			if ($parameter2 !== null)
146
			{
147
				$parameter1 = (int) $parameter1;
148
				$parameter2 = text2words($parameter2);
149
150
				$inserts = array();
151
				foreach ($parameter2 as $word)
152
					$inserts[] = array($word, $parameter1);
153
154
				if (!empty($inserts))
155
					$smcFunc['db_insert']('ignore',
156
						'{db_prefix}log_search_subjects',
157
						array('word' => 'string', 'id_topic' => 'int'),
158
						$inserts,
159
						array('word', 'id_topic')
160
					);
161
			}
162
			break;
163
164
		case 'topic':
165
			if ($parameter1 === true)
166
				updateSettings(array('totalTopics' => true), true);
167
168
			else
169
			{
170
				// Get the number of topics - a SUM is better for InnoDB tables.
171
				// We also ignore the recycle bin here because there will probably be a bunch of one-post topics there.
172
				$result = $smcFunc['db_query']('', '
173
					SELECT SUM(num_topics + unapproved_topics) AS total_topics
174
					FROM {db_prefix}boards' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
175
					WHERE id_board != {int:recycle_board}' : ''),
176
					array(
177
						'recycle_board' => !empty($modSettings['recycle_board']) ? $modSettings['recycle_board'] : 0,
178
					)
179
				);
180
				$row = $smcFunc['db_fetch_assoc']($result);
181
				$smcFunc['db_free_result']($result);
182
183
				updateSettings(array('totalTopics' => $row['total_topics'] === null ? 0 : $row['total_topics']));
184
			}
185
			break;
186
187
		case 'postgroups':
188
			// Parameter two is the updated columns: we should check to see if we base groups off any of these.
189
			if ($parameter2 !== null && !in_array('posts', $parameter2))
190
				return;
191
192
			$postgroups = cache_get_data('updateStats:postgroups', 360);
193
			if ($postgroups == null || $parameter1 == null)
194
			{
195
				// Fetch the postgroups!
196
				$request = $smcFunc['db_query']('', '
197
					SELECT id_group, min_posts
198
					FROM {db_prefix}membergroups
199
					WHERE min_posts != {int:min_posts}',
200
					array(
201
						'min_posts' => -1,
202
					)
203
				);
204
				$postgroups = array();
205
				while ($row = $smcFunc['db_fetch_assoc']($request))
206
					$postgroups[$row['id_group']] = $row['min_posts'];
207
208
				$smcFunc['db_free_result']($request);
209
210
				// Sort them this way because if it's done with MySQL it causes a filesort :(.
211
				arsort($postgroups);
212
213
				cache_put_data('updateStats:postgroups', $postgroups, 360);
214
			}
215
216
			// Oh great, they've screwed their post groups.
217
			if (empty($postgroups))
218
				return;
219
220
			// Set all membergroups from most posts to least posts.
221
			$conditions = '';
222
			$lastMin = 0;
223
			foreach ($postgroups as $id => $min_posts)
224
			{
225
				$conditions .= '
226
					WHEN posts >= ' . $min_posts . (!empty($lastMin) ? ' AND posts <= ' . $lastMin : '') . ' THEN ' . $id;
227
228
				$lastMin = $min_posts;
229
			}
230
231
			// A big fat CASE WHEN... END is faster than a zillion UPDATE's ;).
232
			$smcFunc['db_query']('', '
233
				UPDATE {db_prefix}members
234
				SET id_post_group = CASE ' . $conditions . '
235
				ELSE 0
236
				END' . ($parameter1 != null ? '
237
				WHERE ' . (is_array($parameter1) ? 'id_member IN ({array_int:members})' : 'id_member = {int:members}') : ''),
238
				array(
239
					'members' => $parameter1,
240
				)
241
			);
242
			break;
243
244
		default:
245
			loadLanguage('Errors');
246
			trigger_error(sprintf($txt['invalid_statistic_type'], $type), E_USER_NOTICE);
247
	}
248
}
249
250
/**
251
 * Updates the columns in the members table.
252
 * Assumes the data has been htmlspecialchar'd.
253
 * this function should be used whenever member data needs to be
254
 * updated in place of an UPDATE query.
255
 *
256
 * id_member is either an int or an array of ints to be updated.
257
 *
258
 * data is an associative array of the columns to be updated and their respective values.
259
 * any string values updated should be quoted and slashed.
260
 *
261
 * the value of any column can be '+' or '-', which mean 'increment'
262
 * and decrement, respectively.
263
 *
264
 * if the member's post number is updated, updates their post groups.
265
 *
266
 * @param mixed $members An array of member IDs, the ID of a single member, or null to update this for all members
267
 * @param array $data The info to update for the members
268
 */
269
function updateMemberData($members, $data)
270
{
271
	global $modSettings, $user_info, $smcFunc, $sourcedir, $cache_enable;
272
273
	// An empty array means there's nobody to update.
274
	if ($members === array())
275
		return;
276
277
	$parameters = array();
278
	if (is_array($members))
279
	{
280
		$condition = 'id_member IN ({array_int:members})';
281
		$parameters['members'] = $members;
282
	}
283
284
	elseif ($members === null)
285
		$condition = '1=1';
286
287
	else
288
	{
289
		$condition = 'id_member = {int:member}';
290
		$parameters['member'] = $members;
291
	}
292
293
	// Everything is assumed to be a string unless it's in the below.
294
	$knownInts = array(
295
		'date_registered', 'posts', 'id_group', 'last_login', 'instant_messages', 'unread_messages',
296
		'new_pm', 'pm_prefs', 'gender', 'show_online', 'pm_receive_from', 'alerts',
297
		'id_theme', 'is_activated', 'id_msg_last_visit', 'id_post_group', 'total_time_logged_in', 'warning',
298
	);
299
	$knownFloats = array(
300
		'time_offset',
301
	);
302
303
	if (!empty($modSettings['integrate_change_member_data']))
304
	{
305
		// Only a few member variables are really interesting for integration.
306
		$integration_vars = array(
307
			'member_name',
308
			'real_name',
309
			'email_address',
310
			'id_group',
311
			'gender',
312
			'birthdate',
313
			'website_title',
314
			'website_url',
315
			'location',
316
			'time_format',
317
			'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
	// Make sure $start is a proper variable - not less than 0.
569
	if ($start_invalid)
570
		$start = 0;
571
	// Not greater than the upper bound.
572
	elseif ($start >= $max_value)
573
		$start = max(0, (int) $max_value - (((int) $max_value % (int) $num_per_page) == 0 ? $num_per_page : ((int) $max_value % (int) $num_per_page)));
574
	// And it has to be a multiple of $num_per_page!
575
	else
576
		$start = max(0, (int) $start - ((int) $start % (int) $num_per_page));
577
578
	$context['current_page'] = $start / $num_per_page;
579
580
	// Define some default page index settings if we don't already have it...
581
	if (!isset($settings['page_index']))
582
	{
583
		// This defines the formatting for the page indexes used throughout the forum.
584
		$settings['page_index'] = array(
585
			'extra_before' => '<span class="pages">' . $txt['pages'] . '</span>',
586
			'previous_page' => '<span class="main_icons previous_page"></span>',
587
			'current_page' => '<span class="current_page">%1$d</span> ',
588
			'page' => '<a class="nav_page" href="{URL}">%2$s</a> ',
589
			'expand_pages' => '<span class="expand_pages" onclick="expandPages(this, {LINK}, {FIRST_PAGE}, {LAST_PAGE}, {PER_PAGE});"> ... </span>',
590
			'next_page' => '<span class="main_icons next_page"></span>',
591
			'extra_after' => '',
592
		);
593
	}
594
595
	$base_link = strtr($settings['page_index']['page'], array('{URL}' => $flexible_start ? $base_url : strtr($base_url, array('%' => '%%')) . ';start=%1$d'));
596
	$pageindex = $settings['page_index']['extra_before'];
597
598
	// Compact pages is off or on?
599
	if (empty($modSettings['compactTopicPagesEnable']))
600
	{
601
		// Show the left arrow.
602
		$pageindex .= $start == 0 ? ' ' : sprintf($base_link, $start - $num_per_page, $settings['page_index']['previous_page']);
603
604
		// Show all the pages.
605
		$display_page = 1;
606
		for ($counter = 0; $counter < $max_value; $counter += $num_per_page)
607
			$pageindex .= $start == $counter && !$start_invalid ? sprintf($settings['page_index']['current_page'], $display_page++) : sprintf($base_link, $counter, $display_page++);
608
609
		// Show the right arrow.
610
		$display_page = ($start + $num_per_page) > $max_value ? $max_value : ($start + $num_per_page);
611
		if ($start != $counter - $max_value && !$start_invalid)
612
			$pageindex .= $display_page > $counter - $num_per_page ? ' ' : sprintf($base_link, $display_page, $settings['page_index']['next_page']);
613
	}
614
	else
615
	{
616
		// If they didn't enter an odd value, pretend they did.
617
		$PageContiguous = (int) ($modSettings['compactTopicPagesContiguous'] - ($modSettings['compactTopicPagesContiguous'] % 2)) / 2;
618
619
		// Show the "prev page" link. (>prev page< 1 ... 6 7 [8] 9 10 ... 15 next page)
620
		if (!empty($start) && $show_prevnext)
621
			$pageindex .= sprintf($base_link, $start - $num_per_page, $settings['page_index']['previous_page']);
622
		else
623
			$pageindex .= '';
624
625
		// Show the first page. (prev page >1< ... 6 7 [8] 9 10 ... 15)
626
		if ($start > $num_per_page * $PageContiguous)
627
			$pageindex .= sprintf($base_link, 0, '1');
628
629
		// Show the ... after the first page.  (prev page 1 >...< 6 7 [8] 9 10 ... 15 next page)
630
		if ($start > $num_per_page * ($PageContiguous + 1))
631
			$pageindex .= strtr($settings['page_index']['expand_pages'], array(
632
				'{LINK}' => JavaScriptEscape($smcFunc['htmlspecialchars']($base_link)),
633
				'{FIRST_PAGE}' => $num_per_page,
634
				'{LAST_PAGE}' => $start - $num_per_page * $PageContiguous,
635
				'{PER_PAGE}' => $num_per_page,
636
			));
637
638
		// Show the pages before the current one. (prev page 1 ... >6 7< [8] 9 10 ... 15 next page)
639
		for ($nCont = $PageContiguous; $nCont >= 1; $nCont--)
640
			if ($start >= $num_per_page * $nCont)
641
			{
642
				$tmpStart = $start - $num_per_page * $nCont;
643
				$pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
644
			}
645
646
		// Show the current page. (prev page 1 ... 6 7 >[8]< 9 10 ... 15 next page)
647
		if (!$start_invalid)
648
			$pageindex .= sprintf($settings['page_index']['current_page'], $start / $num_per_page + 1);
649
		else
650
			$pageindex .= sprintf($base_link, $start, $start / $num_per_page + 1);
651
652
		// Show the pages after the current one... (prev page 1 ... 6 7 [8] >9 10< ... 15 next page)
653
		$tmpMaxPages = (int) (($max_value - 1) / $num_per_page) * $num_per_page;
654
		for ($nCont = 1; $nCont <= $PageContiguous; $nCont++)
655
			if ($start + $num_per_page * $nCont <= $tmpMaxPages)
656
			{
657
				$tmpStart = $start + $num_per_page * $nCont;
658
				$pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
659
			}
660
661
		// Show the '...' part near the end. (prev page 1 ... 6 7 [8] 9 10 >...< 15 next page)
662
		if ($start + $num_per_page * ($PageContiguous + 1) < $tmpMaxPages)
663
			$pageindex .= strtr($settings['page_index']['expand_pages'], array(
664
				'{LINK}' => JavaScriptEscape($smcFunc['htmlspecialchars']($base_link)),
665
				'{FIRST_PAGE}' => $start + $num_per_page * ($PageContiguous + 1),
666
				'{LAST_PAGE}' => $tmpMaxPages,
667
				'{PER_PAGE}' => $num_per_page,
668
			));
669
670
		// Show the last number in the list. (prev page 1 ... 6 7 [8] 9 10 ... >15<  next page)
671
		if ($start + $num_per_page * $PageContiguous < $tmpMaxPages)
672
			$pageindex .= sprintf($base_link, $tmpMaxPages, $tmpMaxPages / $num_per_page + 1);
673
674
		// Show the "next page" link. (prev page 1 ... 6 7 [8] 9 10 ... 15 >next page<)
675
		if ($start != $tmpMaxPages && $show_prevnext)
676
			$pageindex .= sprintf($base_link, $start + $num_per_page, $settings['page_index']['next_page']);
677
	}
678
	$pageindex .= $settings['page_index']['extra_after'];
679
680
	return $pageindex;
681
}
682
683
/**
684
 * - Formats a number.
685
 * - uses the format of number_format to decide how to format the number.
686
 *   for example, it might display "1 234,50".
687
 * - caches the formatting data from the setting for optimization.
688
 *
689
 * @param float $number A number
690
 * @param bool|int $override_decimal_count If set, will use the specified number of decimal places. Otherwise it's automatically determined
691
 * @return string A formatted number
692
 */
693
function comma_format($number, $override_decimal_count = false)
694
{
695
	global $txt;
696
	static $thousands_separator = null, $decimal_separator = null, $decimal_count = null;
697
698
	// Cache these values...
699
	if ($decimal_separator === null)
700
	{
701
		// Not set for whatever reason?
702
		if (empty($txt['number_format']) || preg_match('~^1([^\d]*)?234([^\d]*)(0*?)$~', $txt['number_format'], $matches) != 1)
703
			return $number;
704
705
		// Cache these each load...
706
		$thousands_separator = $matches[1];
707
		$decimal_separator = $matches[2];
708
		$decimal_count = strlen($matches[3]);
709
	}
710
711
	// Format the string with our friend, number_format.
712
	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

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

1334
				$substitute_ord = $substitute === '' ? 'none' : mb_ord(/** @scrutinizer ignore-type */ $substitute, $context['character_set']);
Loading history...
1335
1336
				$mb_substitute_character = mb_substitute_character();
1337
				mb_substitute_character($substitute_ord);
1338
1339
				$string = mb_convert_encoding($string, $context['character_set'], $context['character_set']);
1340
1341
				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

1341
				mb_substitute_character(/** @scrutinizer ignore-type */ $mb_substitute_character);
Loading history...
1342
			}
1343
			else
1344
				return false;
1345
		}
1346
	}
1347
1348
	// Fix any weird vertical space characters.
1349
	$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

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

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

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

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

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

2661
		$alltags_regex = '(?' . '>\b' . build_regex(array_unique($alltags)) . '\b|' . /** @scrutinizer ignore-type */ build_regex(array_keys($itemcodes)) . ')';
Loading history...
2662
	}
2663
2664
	$pos = -1;
2665
	while ($pos !== false)
2666
	{
2667
		$last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos;
2668
		preg_match('~\[/?(?=' . $alltags_regex . ')~i', $message, $matches, PREG_OFFSET_CAPTURE, $pos + 1);
2669
		$pos = isset($matches[0][1]) ? $matches[0][1] : false;
2670
2671
		// Failsafe.
2672
		if ($pos === false || $last_pos > $pos)
2673
			$pos = strlen($message) + 1;
2674
2675
		// Can't have a one letter smiley, URL, or email! (Sorry.)
2676
		if ($last_pos < $pos - 1)
2677
		{
2678
			// Make sure the $last_pos is not negative.
2679
			$last_pos = max($last_pos, 0);
2680
2681
			// Pick a block of data to do some raw fixing on.
2682
			$data = substr($message, $last_pos, $pos - $last_pos);
2683
2684
			$placeholders = array();
2685
			$placeholders_counter = 0;
2686
			// Wrap in "private use" Unicode characters to ensure there will be no conflicts.
2687
			$placeholder_template = html_entity_decode('&#xE03C;') . '%1$s' . html_entity_decode('&#xE03E;');
2688
2689
			// Take care of some HTML!
2690
			if (!empty($modSettings['enablePostHTML']) && strpos($data, '&lt;') !== false)
2691
			{
2692
				$data = preg_replace('~&lt;a\s+href=((?:&quot;)?)((?:https?://|ftps?://|mailto:|tel:)\S+?)\\1&gt;(.*?)&lt;/a&gt;~i', '[url=&quot;$2&quot;]$3[/url]', $data);
2693
2694
				// <br> should be empty.
2695
				$empty_tags = array('br', 'hr');
2696
				foreach ($empty_tags as $tag)
2697
					$data = str_replace(array('&lt;' . $tag . '&gt;', '&lt;' . $tag . '/&gt;', '&lt;' . $tag . ' /&gt;'), '<' . $tag . '>', $data);
2698
2699
				// b, u, i, s, pre... basic tags.
2700
				$closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote', 'strong');
2701
				foreach ($closable_tags as $tag)
2702
				{
2703
					$diff = substr_count($data, '&lt;' . $tag . '&gt;') - substr_count($data, '&lt;/' . $tag . '&gt;');
2704
					$data = strtr($data, array('&lt;' . $tag . '&gt;' => '<' . $tag . '>', '&lt;/' . $tag . '&gt;' => '</' . $tag . '>'));
2705
2706
					if ($diff > 0)
2707
						$data = substr($data, 0, -1) . str_repeat('</' . $tag . '>', $diff) . substr($data, -1);
2708
				}
2709
2710
				// Do <img ...> - with security... action= -> action-.
2711
				preg_match_all('~&lt;img\s+src=((?:&quot;)?)((?:https?://|ftps?://)\S+?)\\1(?:\s+alt=(&quot;.*?&quot;|\S*?))?(?:\s?/)?&gt;~i', $data, $matches, PREG_PATTERN_ORDER);
2712
				if (!empty($matches[0]))
2713
				{
2714
					$replaces = array();
2715
					foreach ($matches[2] as $match => $imgtag)
2716
					{
2717
						$alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^&quot;|&quot;$~', '', $matches[3][$match]);
2718
2719
						// Remove action= from the URL - no funny business, now.
2720
						// @todo Testing this preg_match seems pointless
2721
						if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) != 0)
2722
							$imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag);
2723
2724
						$placeholder = sprintf($placeholder_template, ++$placeholders_counter);
2725
						$placeholders[$placeholder] = '[img' . $alt . ']' . $imgtag . '[/img]';
2726
2727
						$replaces[$matches[0][$match]] = $placeholder;
2728
					}
2729
2730
					$data = strtr($data, $replaces);
2731
				}
2732
			}
2733
2734
			if (!empty($modSettings['autoLinkUrls']))
2735
			{
2736
				// Are we inside tags that should be auto linked?
2737
				$no_autolink_area = false;
2738
				if (!empty($open_tags))
2739
				{
2740
					foreach ($open_tags as $open_tag)
2741
						if (in_array($open_tag['tag'], $no_autolink_tags))
2742
							$no_autolink_area = true;
2743
				}
2744
2745
				// Don't go backwards.
2746
				// @todo Don't think is the real solution....
2747
				$lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0;
2748
				if ($pos < $lastAutoPos)
2749
					$no_autolink_area = true;
2750
				$lastAutoPos = $pos;
2751
2752
				if (!$no_autolink_area)
2753
				{
2754
					// An &nbsp; right after a URL can break the autolinker
2755
					if (strpos($data, '&nbsp;') !== false)
2756
					{
2757
						$placeholders[html_entity_decode('&nbsp;', null, $context['character_set'])] = '&nbsp;';
0 ignored issues
show
Bug introduced by
null of type null is incompatible with the type integer expected by parameter $flags of html_entity_decode(). ( Ignorable by Annotation )

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

2757
						$placeholders[html_entity_decode('&nbsp;', /** @scrutinizer ignore-type */ null, $context['character_set'])] = '&nbsp;';
Loading history...
2758
						$data = strtr($data, array('&nbsp;' => html_entity_decode('&nbsp;', null, $context['character_set'])));
2759
					}
2760
2761
					// Some reusable character classes
2762
					$excluded_trailing_chars = '!;:.,?';
2763
					$domain_label_chars = '0-9A-Za-z\-' . ($context['utf8'] ? implode('', array(
2764
						'\x{A0}-\x{D7FF}', '\x{F900}-\x{FDCF}', '\x{FDF0}-\x{FFEF}',
2765
						'\x{10000}-\x{1FFFD}', '\x{20000}-\x{2FFFD}', '\x{30000}-\x{3FFFD}',
2766
						'\x{40000}-\x{4FFFD}', '\x{50000}-\x{5FFFD}', '\x{60000}-\x{6FFFD}',
2767
						'\x{70000}-\x{7FFFD}', '\x{80000}-\x{8FFFD}', '\x{90000}-\x{9FFFD}',
2768
						'\x{A0000}-\x{AFFFD}', '\x{B0000}-\x{BFFFD}', '\x{C0000}-\x{CFFFD}',
2769
						'\x{D0000}-\x{DFFFD}', '\x{E1000}-\x{EFFFD}',
2770
					)) : '');
2771
2772
					// Parse any URLs
2773
					if (!isset($disabled['url']) && strpos($data, '[url') === false)
2774
					{
2775
						// URI schemes that require some sort of special handling.
2776
						$schemes = array(
2777
							// Schemes whose URI definitions require a domain name in the
2778
							// authority (or whatever the next part of the URI is).
2779
							'need_domain' => array(
2780
								'aaa', 'aaas', 'acap', 'acct', 'afp', 'cap', 'cid', 'coap',
2781
								'coap+tcp', 'coap+ws', 'coaps', 'coaps+tcp', 'coaps+ws', 'crid',
2782
								'cvs', 'dict', 'dns', 'feed', 'fish', 'ftp', 'git', 'go',
2783
								'gopher', 'h323', 'http', 'https', 'iax', 'icap', 'im', 'imap',
2784
								'ipp', 'ipps', 'irc', 'irc6', 'ircs', 'ldap', 'ldaps', 'mailto',
2785
								'mid', 'mupdate', 'nfs', 'nntp', 'pop', 'pres', 'reload',
2786
								'rsync', 'rtsp', 'sftp', 'sieve', 'sip', 'sips', 'smb', 'snmp',
2787
								'soap.beep', 'soap.beeps', 'ssh', 'svn', 'stun', 'stuns',
2788
								'telnet', 'tftp', 'tip', 'tn3270', 'turn', 'turns', 'tv', 'udp',
2789
								'vemmi', 'vnc', 'webcal', 'ws', 'wss', 'xmlrpc.beep',
2790
								'xmlrpc.beeps', 'xmpp', 'z39.50', 'z39.50r', 'z39.50s',
2791
							),
2792
							// Schemes that allow an empty authority ("://" followed by "/")
2793
							'empty_authority' => array(
2794
								'file', 'ni', 'nih',
2795
							),
2796
							// Schemes that do not use an authority but still have a reasonable
2797
							// chance of working as clickable links.
2798
							'no_authority' => array(
2799
								'about', 'callto', 'geo', 'gg', 'leaptofrogans', 'magnet',
2800
								'mailto', 'maps', 'news', 'ni', 'nih', 'service', 'skype',
2801
								'sms', 'tel', 'tv',
2802
							),
2803
							// Schemes that we should never link.
2804
							'forbidden' => array(
2805
								'javascript', 'data',
2806
							),
2807
						);
2808
2809
						// In case a mod wants to control behaviour for a special URI scheme.
2810
						call_integration_hook('integrate_autolinker_schemes', array(&$schemes));
2811
2812
						// Don't repeat this unnecessarily.
2813
						if (empty($url_regex))
2814
						{
2815
							// PCRE subroutines for efficiency.
2816
							$pcre_subroutines = array(
2817
								'tlds' => $modSettings['tld_regex'],
2818
								'pct' => '%[0-9A-Fa-f]{2}',
2819
								'domain_label_char' => '[' . $domain_label_chars . ']',
2820
								'not_domain_label_char' => '[^' . $domain_label_chars . ']',
2821
								'domain' => '(?:(?P>domain_label_char)+\.)+(?P>tlds)',
2822
								'no_domain' => '(?:(?P>domain_label_char)|[._\~!$&\'()*+,;=:@]|(?P>pct))+',
2823
								'scheme_need_domain' => build_regex($schemes['need_domain'], '~'),
2824
								'scheme_empty_authority' => build_regex($schemes['empty_authority'], '~'),
2825
								'scheme_no_authority' => build_regex($schemes['no_authority'], '~'),
2826
								'scheme_any' => '[A-Za-z][0-9A-Za-z+\-.]*',
2827
								'user_info' => '(?:(?P>domain_label_char)|[._\~!$&\'()*+,;=:]|(?P>pct))+',
2828
								'dec_octet' => '(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)',
2829
								'h16' => '[0-9A-Fa-f]{1,4}',
2830
								'ipv4' => '(?:\b(?:(?P>dec_octet)\.){3}(?P>dec_octet)\b)',
2831
								'ipv6' => '\[(?:' . implode('|', array(
2832
									'(?:(?P>h16):){7}(?P>h16)',
2833
									'(?:(?P>h16):){1,7}:',
2834
									'(?:(?P>h16):){1,6}(?::(?P>h16))',
2835
									'(?:(?P>h16):){1,5}(?::(?P>h16)){1,2}',
2836
									'(?:(?P>h16):){1,4}(?::(?P>h16)){1,3}',
2837
									'(?:(?P>h16):){1,3}(?::(?P>h16)){1,4}',
2838
									'(?:(?P>h16):){1,2}(?::(?P>h16)){1,5}',
2839
									'(?P>h16):(?::(?P>h16)){1,6}',
2840
									':(?:(?::(?P>h16)){1,7}|:)',
2841
									'fe80:(?::(?P>h16)){0,4}%[0-9A-Za-z]+',
2842
									'::(ffff(:0{1,4})?:)?(?P>ipv4)',
2843
									'(?:(?P>h16):){1,4}:(?P>ipv4)',
2844
								)) . ')\]',
2845
								'host' => '(?:' . implode('|', array(
2846
									'localhost',
2847
									'(?P>domain)',
2848
									'(?P>ipv4)',
2849
									'(?P>ipv6)',
2850
								)) . ')',
2851
								'authority' => '(?:(?P>user_info)@)?(?P>host)(?::\d+)?',
2852
							);
2853
2854
							// Brackets and quotation marks are problematic at the end of an IRI.
2855
							// E.g.: `http://foo.com/baz(qux)` vs. `(http://foo.com/baz_qux)`
2856
							// In the first case, the user probably intended the `)` as part of the
2857
							// IRI, but not in the second case. To account for this, we test for
2858
							// balanced pairs within the IRI.
2859
							$balanced_pairs = array(
2860
								// Brackets and parentheses
2861
								'(' => ')', '[' => ']', '{' => '}',
2862
								// Double quotation marks
2863
								'"' => '"',
2864
								html_entity_decode('&#x201C;', null, $context['character_set']) => html_entity_decode('&#x201D;', null, $context['character_set']),
2865
								html_entity_decode('&#x201E;', null, $context['character_set']) => html_entity_decode('&#x201D;', null, $context['character_set']),
2866
								html_entity_decode('&#x201F;', null, $context['character_set']) => html_entity_decode('&#x201D;', null, $context['character_set']),
2867
								html_entity_decode('&#x00AB;', null, $context['character_set']) => html_entity_decode('&#x00BB;', null, $context['character_set']),
2868
								// Single quotation marks
2869
								'\'' => '\'',
2870
								html_entity_decode('&#x2018;', null, $context['character_set']) => html_entity_decode('&#x2019;', null, $context['character_set']),
2871
								html_entity_decode('&#x201A;', null, $context['character_set']) => html_entity_decode('&#x2019;', null, $context['character_set']),
2872
								html_entity_decode('&#x201B;', null, $context['character_set']) => html_entity_decode('&#x2019;', null, $context['character_set']),
2873
								html_entity_decode('&#x2039;', null, $context['character_set']) => html_entity_decode('&#x203A;', null, $context['character_set']),
2874
							);
2875
							foreach ($balanced_pairs as $pair_opener => $pair_closer)
2876
								$balanced_pairs[$smcFunc['htmlspecialchars']($pair_opener)] = $smcFunc['htmlspecialchars']($pair_closer);
2877
2878
							$bracket_quote_chars = '';
2879
							$bracket_quote_entities = array();
2880
							foreach ($balanced_pairs as $pair_opener => $pair_closer)
2881
							{
2882
								if ($pair_opener == $pair_closer)
2883
									$pair_closer = '';
2884
2885
								foreach (array($pair_opener, $pair_closer) as $bracket_quote)
2886
								{
2887
									if (strpos($bracket_quote, '&') === false)
2888
										$bracket_quote_chars .= $bracket_quote;
2889
									else
2890
										$bracket_quote_entities[] = substr($bracket_quote, 1);
2891
								}
2892
							}
2893
							$bracket_quote_chars = str_replace(array('[', ']'), array('\[', '\]'), $bracket_quote_chars);
2894
2895
							$pcre_subroutines['bracket_quote'] = '[' . $bracket_quote_chars . ']|&' . build_regex($bracket_quote_entities, '~');
0 ignored issues
show
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

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

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

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

3138
									if (!$can_validate || filter_var($parsedurl['path'], FILTER_VALIDATE_EMAIL, /** @scrutinizer ignore-type */ $flags) !== false)
Loading history...
3139
										return '[email=' . str_replace('mailto:', '', $url) . ']' . $url . '[/email]';
3140
									else
3141
										return $url;
3142
								}
3143
3144
								// Are we linking a schemeless URL or naked domain name (e.g. "example.com")?
3145
								if (empty($parsedurl['scheme']))
3146
									$fullUrl = '//' . ltrim($url, ':/');
3147
								else
3148
									$fullUrl = $url;
3149
3150
								// Make sure that $fullUrl really is valid
3151
								if (in_array($parsedurl['scheme'], $schemes['forbidden']) || (!in_array($parsedurl['scheme'], $schemes['no_authority']) && validate_iri((strpos($fullUrl, '//') === 0 ? 'http:' : '') . $fullUrl) === false))
3152
									return $url;
3153
3154
								return '[url=&quot;' . str_replace(array('[', ']'), array('&#91;', '&#93;'), iri_to_url($fullUrl)) . '&quot;]' . $url . '[/url]';
3155
							},
3156
							$data
3157
						);
3158
3159
						if (!is_null($tmp_data))
3160
							$data = $tmp_data;
3161
					}
3162
3163
					// Next, emails...  Must be careful not to step on enablePostHTML logic above...
3164
					if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false && stripos($data, 'mailto:') === false)
3165
					{
3166
						// Preceded by a space or start of line
3167
						$email_regex = '(?<=^|<br>|[\h\v])' .
3168
3169
						// An email address
3170
						'[' . $domain_label_chars . '_.]{1,80}' .
3171
						'@' .
3172
						'[' . $domain_label_chars . '.]+' .
3173
						'\.' . $modSettings['tld_regex'] .
3174
3175
						// Followed by a non-domain character or end of line
3176
						'(?=[^' . $domain_label_chars . ']|$)';
3177
3178
						$tmp_data = preg_replace('~' . $email_regex . '~i' . ($context['utf8'] ? 'u' : ''), '[email]$0[/email]', $data);
3179
3180
						if (!is_null($tmp_data))
3181
							$data = $tmp_data;
3182
					}
3183
3184
					// Save a little memory.
3185
					unset($tmp_data);
3186
				}
3187
			}
3188
3189
			// Restore any placeholders
3190
			$data = strtr($data, $placeholders);
3191
3192
			$data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));
3193
3194
			// If it wasn't changed, no copying or other boring stuff has to happen!
3195
			if ($data != substr($message, $last_pos, $pos - $last_pos))
3196
			{
3197
				$message = substr($message, 0, $last_pos) . $data . substr($message, $pos);
3198
3199
				// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
3200
				$old_pos = strlen($data) + $last_pos;
3201
				$pos = strpos($message, '[', $last_pos);
3202
				$pos = $pos === false ? $old_pos : min($pos, $old_pos);
3203
			}
3204
		}
3205
3206
		// Are we there yet?  Are we there yet?
3207
		if ($pos >= strlen($message) - 1)
3208
			break;
3209
3210
		$tag_character = strtolower($message[$pos + 1]);
3211
3212
		if ($tag_character == '/' && !empty($open_tags))
3213
		{
3214
			$pos2 = strpos($message, ']', $pos + 1);
3215
			if ($pos2 == $pos + 2)
3216
				continue;
3217
3218
			$look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));
3219
3220
			// A closing tag that doesn't match any open tags? Skip it.
3221
			if (!in_array($look_for, array_map(function($code) { return $code['tag']; }, $open_tags)))
3222
				continue;
3223
3224
			$to_close = array();
3225
			$block_level = null;
3226
3227
			do
3228
			{
3229
				$tag = array_pop($open_tags);
3230
				if (!$tag)
3231
					break;
3232
3233
				if (!empty($tag['block_level']))
3234
				{
3235
					// Only find out if we need to.
3236
					if ($block_level === false)
3237
					{
3238
						array_push($open_tags, $tag);
3239
						break;
3240
					}
3241
3242
					// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
3243
					if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]]))
3244
					{
3245
						foreach ($bbc_codes[$look_for[0]] as $temp)
3246
							if ($temp['tag'] == $look_for)
3247
							{
3248
								$block_level = !empty($temp['block_level']);
3249
								break;
3250
							}
3251
					}
3252
3253
					if ($block_level !== true)
3254
					{
3255
						$block_level = false;
3256
						array_push($open_tags, $tag);
3257
						break;
3258
					}
3259
				}
3260
3261
				$to_close[] = $tag;
3262
			}
3263
			while ($tag['tag'] != $look_for);
3264
3265
			// Did we just eat through everything and not find it?
3266
			if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
3267
			{
3268
				$open_tags = $to_close;
3269
				continue;
3270
			}
3271
			elseif (!empty($to_close) && $tag['tag'] != $look_for)
3272
			{
3273
				if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]]))
3274
				{
3275
					foreach ($bbc_codes[$look_for[0]] as $temp)
3276
						if ($temp['tag'] == $look_for)
3277
						{
3278
							$block_level = !empty($temp['block_level']);
3279
							break;
3280
						}
3281
				}
3282
3283
				// We're not looking for a block level tag (or maybe even a tag that exists...)
3284
				if (!$block_level)
0 ignored issues
show
Bug Best Practice introduced by
The expression $block_level of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

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

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

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

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

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

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

5251
		if (strpos(/** @scrutinizer ignore-type */ $test, 'not found') !== false)
Loading history...
5252
			$host = '';
5253
		// Invalid server option?
5254
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
5255
			updateSettings(array('host_to_dis' => 1));
5256
		// Maybe it found something, after all?
5257
		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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

7381
	elseif (!empty($modSettings['tld_regex']) && @preg_match('~' . $modSettings['tld_regex'] . '~', /** @scrutinizer ignore-type */ null) !== false)
Loading history...
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 = null)
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)
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

7889
	if (filter_var($url, FILTER_VALIDATE_URL, /** @scrutinizer ignore-type */ $flags) !== false)
Loading history...
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
		$normalized_host = '';
7959
		$pos = 0;
7960
	}
7961
7962
	$before_host = substr($iri, 0, $pos);
7963
	$after_host = substr($iri, $pos + strlen($host));
7964
7965
	return $before_host . $normalized_host . $after_host;
7966
}
7967
7968
/**
7969
 * Converts a URL with international characters (an IRI) into a pure ASCII URL
7970
 *
7971
 * Uses Punycode to encode any non-ASCII characters in the domain name, and uses
7972
 * standard URL encoding on the rest.
7973
 *
7974
 * @param string $iri A IRI that may or may not contain non-ASCII characters.
7975
 * @return string|bool The URL version of the IRI.
7976
 */
7977
function iri_to_url($iri)
7978
{
7979
	global $sourcedir, $context, $txt, $db_character_set;
7980
7981
	// Sanity check: must be using UTF-8 to do this.
7982
	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')))
7983
		return $iri;
7984
7985
	require_once($sourcedir . '/Subs-Charset.php');
7986
7987
	$iri = sanitize_iri(utf8_normalize_c($iri));
7988
7989
	$host = parse_iri((strpos($iri, '//') === 0 ? 'http:' : '') . $iri, PHP_URL_HOST);
7990
7991
	if (!empty($host))
7992
	{
7993
		if (!function_exists('idn_to_ascii'))
7994
			require_once($sourcedir . '/Subs-Compat.php');
7995
7996
		// Convert the host using the Punycode algorithm
7997
		$encoded_host = idn_to_ascii($host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
7998
7999
		$pos = strpos($iri, $host);
8000
	}
8001
	else
8002
	{
8003
		$encoded_host = '';
8004
		$pos = 0;
8005
	}
8006
8007
	$before_host = substr($iri, 0, $pos);
8008
	$after_host = substr($iri, $pos + strlen($host));
8009
8010
	// Encode any disallowed characters in the rest of the URL
8011
	$unescaped = array(
8012
		'%21' => '!', '%23' => '#', '%24' => '$', '%26' => '&',
8013
		'%27' => "'", '%28' => '(', '%29' => ')', '%2A' => '*',
8014
		'%2B' => '+', '%2C' => ',', '%2F' => '/', '%3A' => ':',
8015
		'%3B' => ';', '%3D' => '=', '%3F' => '?', '%40' => '@',
8016
		'%25' => '%',
8017
	);
8018
8019
	$before_host = strtr(rawurlencode($before_host), $unescaped);
8020
	$after_host = strtr(rawurlencode($after_host), $unescaped);
8021
8022
	return $before_host . $encoded_host . $after_host;
8023
}
8024
8025
/**
8026
 * Decodes a URL containing encoded international characters to UTF-8
8027
 *
8028
 * Decodes any Punycode encoded characters in the domain name, then uses
8029
 * standard URL decoding on the rest.
8030
 *
8031
 * @param string $url The pure ASCII version of a URL.
8032
 * @return string|bool The UTF-8 version of the URL.
8033
 */
8034
function url_to_iri($url)
8035
{
8036
	global $sourcedir, $context, $txt, $db_character_set;
8037
8038
	// Sanity check: must be using UTF-8 to do this.
8039
	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')))
8040
		return $url;
8041
8042
	$host = parse_iri((strpos($url, '//') === 0 ? 'http:' : '') . $url, PHP_URL_HOST);
8043
8044
	if (!empty($host))
8045
	{
8046
		if (!function_exists('idn_to_utf8'))
8047
			require_once($sourcedir . '/Subs-Compat.php');
8048
8049
		// Decode the domain from Punycode
8050
		$decoded_host = idn_to_utf8($host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
8051
8052
		$pos = strpos($url, $host);
8053
	}
8054
	else
8055
	{
8056
		$decoded_host = '';
8057
		$pos = 0;
8058
	}
8059
8060
	$before_host = substr($url, 0, $pos);
8061
	$after_host = substr($url, $pos + strlen($host));
8062
8063
	// Decode the rest of the URL, but preserve escaped URL syntax characters.
8064
	$double_escaped = array(
8065
		'%21' => '%2521', '%23' => '%2523', '%24' => '%2524', '%26' => '%2526',
8066
		'%27' => '%2527', '%28' => '%2528', '%29' => '%2529', '%2A' => '%252A',
8067
		'%2B' => '%252B', '%2C' => '%252C', '%2F' => '%252F', '%3A' => '%253A',
8068
		'%3B' => '%253B', '%3D' => '%253D', '%3F' => '%253F', '%40' => '%2540',
8069
		'%25' => '%2525',
8070
	);
8071
8072
	$before_host = rawurldecode(strtr($before_host, $double_escaped));
8073
	$after_host = rawurldecode(strtr($after_host, $double_escaped));
8074
8075
	return $before_host . $decoded_host . $after_host;
8076
}
8077
8078
/**
8079
 * Ensures SMF's scheduled tasks are being run as intended
8080
 *
8081
 * If the admin activated the cron_is_real_cron setting, but the cron job is
8082
 * not running things at least once per day, we need to go back to SMF's default
8083
 * behaviour using "web cron" JavaScript calls.
8084
 */
8085
function check_cron()
8086
{
8087
	global $modSettings, $smcFunc, $txt;
8088
8089
	if (!empty($modSettings['cron_is_real_cron']) && time() - @intval($modSettings['cron_last_checked']) > 84600)
8090
	{
8091
		$request = $smcFunc['db_query']('', '
8092
			SELECT COUNT(*)
8093
			FROM {db_prefix}scheduled_tasks
8094
			WHERE disabled = {int:not_disabled}
8095
				AND next_time < {int:yesterday}',
8096
			array(
8097
				'not_disabled' => 0,
8098
				'yesterday' => time() - 84600,
8099
			)
8100
		);
8101
		list($overdue) = $smcFunc['db_fetch_row']($request);
8102
		$smcFunc['db_free_result']($request);
8103
8104
		// If we have tasks more than a day overdue, cron isn't doing its job.
8105
		if (!empty($overdue))
8106
		{
8107
			loadLanguage('ManageScheduledTasks');
8108
			log_error($txt['cron_not_working']);
8109
			updateSettings(array('cron_is_real_cron' => 0));
8110
		}
8111
		else
8112
			updateSettings(array('cron_last_checked' => time()));
8113
	}
8114
}
8115
8116
/**
8117
 * Sends an appropriate HTTP status header based on a given status code
8118
 *
8119
 * @param int $code The status code
8120
 * @param string $status The string for the status. Set automatically if not provided.
8121
 */
8122
function send_http_status($code, $status = '')
8123
{
8124
	global $sourcedir;
8125
8126
	$statuses = array(
8127
		204 => 'No Content',
8128
		206 => 'Partial Content',
8129
		304 => 'Not Modified',
8130
		400 => 'Bad Request',
8131
		403 => 'Forbidden',
8132
		404 => 'Not Found',
8133
		410 => 'Gone',
8134
		500 => 'Internal Server Error',
8135
		503 => 'Service Unavailable',
8136
	);
8137
8138
	$protocol = preg_match('~^\s*(HTTP/[12]\.\d)\s*$~i', $_SERVER['SERVER_PROTOCOL'], $matches) ? $matches[1] : 'HTTP/1.0';
8139
8140
	// Typically during these requests, we have cleaned the response (ob_*clean), ensure these headers exist.
8141
	require_once($sourcedir . '/Security.php');
8142
	frameOptionsHeader();
8143
	corsPolicyHeader();
8144
8145
	if (!isset($statuses[$code]) && empty($status))
8146
		header($protocol . ' 500 Internal Server Error');
8147
	else
8148
		header($protocol . ' ' . $code . ' ' . (!empty($status) ? $status : $statuses[$code]));
8149
}
8150
8151
/**
8152
 * Concatenates an array of strings into a grammatically correct sentence list
8153
 *
8154
 * Uses formats defined in the language files to build the list appropropriately
8155
 * for the currently loaded language.
8156
 *
8157
 * @param array $list An array of strings to concatenate.
8158
 * @return string The localized sentence list.
8159
 */
8160
function sentence_list($list)
8161
{
8162
	global $txt;
8163
8164
	// Make sure the bare necessities are defined
8165
	if (empty($txt['sentence_list_format']['n']))
8166
		$txt['sentence_list_format']['n'] = '{series}';
8167
	if (!isset($txt['sentence_list_separator']))
8168
		$txt['sentence_list_separator'] = ', ';
8169
	if (!isset($txt['sentence_list_separator_alt']))
8170
		$txt['sentence_list_separator_alt'] = '; ';
8171
8172
	// Which format should we use?
8173
	if (isset($txt['sentence_list_format'][count($list)]))
8174
		$format = $txt['sentence_list_format'][count($list)];
8175
	else
8176
		$format = $txt['sentence_list_format']['n'];
8177
8178
	// Do we want the normal separator or the alternate?
8179
	$separator = $txt['sentence_list_separator'];
8180
	foreach ($list as $item)
8181
	{
8182
		if (strpos($item, $separator) !== false)
8183
		{
8184
			$separator = $txt['sentence_list_separator_alt'];
8185
			$format = strtr($format, trim($txt['sentence_list_separator']), trim($separator));
8186
			break;
8187
		}
8188
	}
8189
8190
	$replacements = array();
8191
8192
	// Special handling for the last items on the list
8193
	$i = 0;
8194
	while (empty($done))
8195
	{
8196
		if (strpos($format, '{'. --$i . '}') !== false)
8197
			$replacements['{'. $i . '}'] = array_pop($list);
8198
		else
8199
			$done = true;
8200
	}
8201
	unset($done);
8202
8203
	// Special handling for the first items on the list
8204
	$i = 0;
8205
	while (empty($done))
8206
	{
8207
		if (strpos($format, '{'. ++$i . '}') !== false)
8208
			$replacements['{'. $i . '}'] = array_shift($list);
8209
		else
8210
			$done = true;
8211
	}
8212
	unset($done);
8213
8214
	// Whatever is left
8215
	$replacements['{series}'] = implode($separator, $list);
8216
8217
	// Do the deed
8218
	return strtr($format, $replacements);
8219
}
8220
8221
/**
8222
 * Truncate an array to a specified length
8223
 *
8224
 * @param array $array The array to truncate
8225
 * @param int $max_length The upperbound on the length
8226
 * @param int $deep How levels in an multidimensional array should the function take into account.
8227
 * @return array The truncated array
8228
 */
8229
function truncate_array($array, $max_length = 1900, $deep = 3)
8230
{
8231
	$array = (array) $array;
8232
8233
	$curr_length = array_length($array, $deep);
8234
8235
	if ($curr_length <= $max_length)
8236
		return $array;
8237
8238
	else
8239
	{
8240
		// Truncate each element's value to a reasonable length
8241
		$param_max = floor($max_length / count($array));
8242
8243
		$current_deep = $deep - 1;
8244
8245
		foreach ($array as $key => &$value)
8246
		{
8247
			if (is_array($value))
8248
				if ($current_deep > 0)
8249
					$value = truncate_array($value, $current_deep);
8250
8251
			else
8252
				$value = substr($value, 0, $param_max - strlen($key) - 5);
0 ignored issues
show
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

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

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