Passed
Pull Request — release-2.1 (#7179)
by Jon
04:22
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
nc 4744
nop 3
dl 0
loc 311
rs 0
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file has all the main functions in it that relate to, well, everything.
5
 *
6
 * Simple Machines Forum (SMF)
7
 *
8
 * @package SMF
9
 * @author Simple Machines https://www.simplemachines.org
10
 * @copyright 2021 Simple Machines and individual contributors
11
 * @license https://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1 RC4
14
 */
15
16
if (!defined('SMF'))
17
	die('No direct access...');
18
19
/**
20
 * Update some basic statistics.
21
 *
22
 * 'member' statistic updates the latest member, the total member
23
 *  count, and the number of unapproved members.
24
 * 'member' also only counts approved members when approval is on, but
25
 *  is much more efficient with it off.
26
 *
27
 * 'message' changes the total number of messages, and the
28
 *  highest message id by id_msg - which can be parameters 1 and 2,
29
 *  respectively.
30
 *
31
 * 'topic' updates the total number of topics, or if parameter1 is true
32
 *  simply increments them.
33
 *
34
 * 'subject' updates the log_search_subjects in the event of a topic being
35
 *  moved, removed or split.  parameter1 is the topicid, parameter2 is the new subject
36
 *
37
 * 'postgroups' case updates those members who match condition's
38
 *  post-based membergroups in the database (restricted by parameter1).
39
 *
40
 * @param string $type Stat type - can be 'member', 'message', 'topic', 'subject' or 'postgroups'
41
 * @param mixed $parameter1 A parameter for updating the stats
42
 * @param mixed $parameter2 A 2nd parameter for updating the stats
43
 */
44
function updateStats($type, $parameter1 = null, $parameter2 = null)
45
{
46
	global $modSettings, $smcFunc, $txt;
47
48
	switch ($type)
49
	{
50
		case 'member':
51
			$changes = array(
52
				'memberlist_updated' => time(),
53
			);
54
55
			// #1 latest member ID, #2 the real name for a new registration.
56
			if (is_numeric($parameter1))
57
			{
58
				$changes['latestMember'] = $parameter1;
59
				$changes['latestRealName'] = $parameter2;
60
61
				updateSettings(array('totalMembers' => true), true);
62
			}
63
64
			// We need to calculate the totals.
65
			else
66
			{
67
				// Update the latest activated member (highest id_member) and count.
68
				$result = $smcFunc['db_query']('', '
69
					SELECT COUNT(*), MAX(id_member)
70
					FROM {db_prefix}members
71
					WHERE is_activated = {int:is_activated}',
72
					array(
73
						'is_activated' => 1,
74
					)
75
				);
76
				list ($changes['totalMembers'], $changes['latestMember']) = $smcFunc['db_fetch_row']($result);
77
				$smcFunc['db_free_result']($result);
78
79
				// Get the latest activated member's display name.
80
				$result = $smcFunc['db_query']('', '
81
					SELECT real_name
82
					FROM {db_prefix}members
83
					WHERE id_member = {int:id_member}
84
					LIMIT 1',
85
					array(
86
						'id_member' => (int) $changes['latestMember'],
87
					)
88
				);
89
				list ($changes['latestRealName']) = $smcFunc['db_fetch_row']($result);
90
				$smcFunc['db_free_result']($result);
91
92
				// Update the amount of members awaiting approval
93
				$result = $smcFunc['db_query']('', '
94
					SELECT COUNT(*)
95
					FROM {db_prefix}members
96
					WHERE is_activated IN ({array_int:activation_status})',
97
					array(
98
						'activation_status' => array(3, 4, 5),
99
					)
100
				);
101
102
				list ($changes['unapprovedMembers']) = $smcFunc['db_fetch_row']($result);
103
				$smcFunc['db_free_result']($result);
104
			}
105
			updateSettings($changes);
106
			break;
107
108
		case 'message':
109
			if ($parameter1 === true && $parameter2 !== null)
110
				updateSettings(array('totalMessages' => true, 'maxMsgID' => $parameter2), true);
111
			else
112
			{
113
				// SUM and MAX on a smaller table is better for InnoDB tables.
114
				$result = $smcFunc['db_query']('', '
115
					SELECT SUM(num_posts + unapproved_posts) AS total_messages, MAX(id_last_msg) AS max_msg_id
116
					FROM {db_prefix}boards
117
					WHERE redirect = {string:blank_redirect}' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
118
						AND id_board != {int:recycle_board}' : ''),
119
					array(
120
						'recycle_board' => isset($modSettings['recycle_board']) ? $modSettings['recycle_board'] : 0,
121
						'blank_redirect' => '',
122
					)
123
				);
124
				$row = $smcFunc['db_fetch_assoc']($result);
125
				$smcFunc['db_free_result']($result);
126
127
				updateSettings(array(
128
					'totalMessages' => $row['total_messages'] === null ? 0 : $row['total_messages'],
129
					'maxMsgID' => $row['max_msg_id'] === null ? 0 : $row['max_msg_id']
130
				));
131
			}
132
			break;
133
134
		case 'subject':
135
			// Remove the previous subject (if any).
136
			$smcFunc['db_query']('', '
137
				DELETE FROM {db_prefix}log_search_subjects
138
				WHERE id_topic = {int:id_topic}',
139
				array(
140
					'id_topic' => (int) $parameter1,
141
				)
142
			);
143
144
			// Insert the new subject.
145
			if ($parameter2 !== null)
146
			{
147
				$parameter1 = (int) $parameter1;
148
				$parameter2 = text2words($parameter2);
149
150
				$inserts = array();
151
				foreach ($parameter2 as $word)
152
					$inserts[] = array($word, $parameter1);
153
154
				if (!empty($inserts))
155
					$smcFunc['db_insert']('ignore',
156
						'{db_prefix}log_search_subjects',
157
						array('word' => 'string', 'id_topic' => 'int'),
158
						$inserts,
159
						array('word', 'id_topic')
160
					);
161
			}
162
			break;
163
164
		case 'topic':
165
			if ($parameter1 === true)
166
				updateSettings(array('totalTopics' => true), true);
167
168
			else
169
			{
170
				// Get the number of topics - a SUM is better for InnoDB tables.
171
				// We also ignore the recycle bin here because there will probably be a bunch of one-post topics there.
172
				$result = $smcFunc['db_query']('', '
173
					SELECT SUM(num_topics + unapproved_topics) AS total_topics
174
					FROM {db_prefix}boards' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
175
					WHERE id_board != {int:recycle_board}' : ''),
176
					array(
177
						'recycle_board' => !empty($modSettings['recycle_board']) ? $modSettings['recycle_board'] : 0,
178
					)
179
				);
180
				$row = $smcFunc['db_fetch_assoc']($result);
181
				$smcFunc['db_free_result']($result);
182
183
				updateSettings(array('totalTopics' => $row['total_topics'] === null ? 0 : $row['total_topics']));
184
			}
185
			break;
186
187
		case 'postgroups':
188
			// Parameter two is the updated columns: we should check to see if we base groups off any of these.
189
			if ($parameter2 !== null && !in_array('posts', $parameter2))
190
				return;
191
192
			$postgroups = cache_get_data('updateStats:postgroups', 360);
193
			if ($postgroups == null || $parameter1 == null)
194
			{
195
				// Fetch the postgroups!
196
				$request = $smcFunc['db_query']('', '
197
					SELECT id_group, min_posts
198
					FROM {db_prefix}membergroups
199
					WHERE min_posts != {int:min_posts}',
200
					array(
201
						'min_posts' => -1,
202
					)
203
				);
204
				$postgroups = array();
205
				while ($row = $smcFunc['db_fetch_assoc']($request))
206
					$postgroups[$row['id_group']] = $row['min_posts'];
207
208
				$smcFunc['db_free_result']($request);
209
210
				// Sort them this way because if it's done with MySQL it causes a filesort :(.
211
				arsort($postgroups);
212
213
				cache_put_data('updateStats:postgroups', $postgroups, 360);
214
			}
215
216
			// Oh great, they've screwed their post groups.
217
			if (empty($postgroups))
218
				return;
219
220
			// Set all membergroups from most posts to least posts.
221
			$conditions = '';
222
			$lastMin = 0;
223
			foreach ($postgroups as $id => $min_posts)
224
			{
225
				$conditions .= '
226
					WHEN posts >= ' . $min_posts . (!empty($lastMin) ? ' AND posts <= ' . $lastMin : '') . ' THEN ' . $id;
227
228
				$lastMin = $min_posts;
229
			}
230
231
			// A big fat CASE WHEN... END is faster than a zillion UPDATE's ;).
232
			$smcFunc['db_query']('', '
233
				UPDATE {db_prefix}members
234
				SET id_post_group = CASE ' . $conditions . '
235
				ELSE 0
236
				END' . ($parameter1 != null ? '
237
				WHERE ' . (is_array($parameter1) ? 'id_member IN ({array_int:members})' : 'id_member = {int:members}') : ''),
238
				array(
239
					'members' => $parameter1,
240
				)
241
			);
242
			break;
243
244
		default:
245
			loadLanguage('Errors');
246
			trigger_error(sprintf($txt['invalid_statistic_type'], $type), E_USER_NOTICE);
247
	}
248
}
249
250
/**
251
 * Updates the columns in the members table.
252
 * Assumes the data has been htmlspecialchar'd.
253
 * this function should be used whenever member data needs to be
254
 * updated in place of an UPDATE query.
255
 *
256
 * id_member is either an int or an array of ints to be updated.
257
 *
258
 * data is an associative array of the columns to be updated and their respective values.
259
 * any string values updated should be quoted and slashed.
260
 *
261
 * the value of any column can be '+' or '-', which mean 'increment'
262
 * and decrement, respectively.
263
 *
264
 * if the member's post number is updated, updates their post groups.
265
 *
266
 * @param mixed $members An array of member IDs, the ID of a single member, or null to update this for all members
267
 * @param array $data The info to update for the members
268
 */
269
function updateMemberData($members, $data)
270
{
271
	global $modSettings, $user_info, $smcFunc, $sourcedir, $cache_enable;
272
273
	// An empty array means there's nobody to update.
274
	if ($members === array())
275
		return;
276
277
	$parameters = array();
278
	if (is_array($members))
279
	{
280
		$condition = 'id_member IN ({array_int:members})';
281
		$parameters['members'] = $members;
282
	}
283
284
	elseif ($members === null)
285
		$condition = '1=1';
286
287
	else
288
	{
289
		$condition = 'id_member = {int:member}';
290
		$parameters['member'] = $members;
291
	}
292
293
	// Everything is assumed to be a string unless it's in the below.
294
	$knownInts = array(
295
		'date_registered', 'posts', 'id_group', 'last_login', 'instant_messages', 'unread_messages',
296
		'new_pm', 'pm_prefs', 'gender', 'show_online', 'pm_receive_from', 'alerts',
297
		'id_theme', 'is_activated', 'id_msg_last_visit', 'id_post_group', 'total_time_logged_in', 'warning',
298
	);
299
	$knownFloats = array(
300
		'time_offset',
301
	);
302
303
	if (!empty($modSettings['integrate_change_member_data']))
304
	{
305
		// Only a few member variables are really interesting for integration.
306
		$integration_vars = array(
307
			'member_name',
308
			'real_name',
309
			'email_address',
310
			'id_group',
311
			'gender',
312
			'birthdate',
313
			'website_title',
314
			'website_url',
315
			'location',
316
			'time_format',
317
			'time_offset',
318
			'avatar',
319
			'lngfile',
320
		);
321
		$vars_to_integrate = array_intersect($integration_vars, array_keys($data));
322
323
		// Only proceed if there are any variables left to call the integration function.
324
		if (count($vars_to_integrate) != 0)
325
		{
326
			// Fetch a list of member_names if necessary
327
			if ((!is_array($members) && $members === $user_info['id']) || (is_array($members) && count($members) == 1 && in_array($user_info['id'], $members)))
328
				$member_names = array($user_info['username']);
329
			else
330
			{
331
				$member_names = array();
332
				$request = $smcFunc['db_query']('', '
333
					SELECT member_name
334
					FROM {db_prefix}members
335
					WHERE ' . $condition,
336
					$parameters
337
				);
338
				while ($row = $smcFunc['db_fetch_assoc']($request))
339
					$member_names[] = $row['member_name'];
340
				$smcFunc['db_free_result']($request);
341
			}
342
343
			if (!empty($member_names))
344
				foreach ($vars_to_integrate as $var)
345
					call_integration_hook('integrate_change_member_data', array($member_names, $var, &$data[$var], &$knownInts, &$knownFloats));
346
		}
347
	}
348
349
	$setString = '';
350
	foreach ($data as $var => $val)
351
	{
352
		switch ($var)
353
		{
354
			case  'birthdate':
355
				$type = 'date';
356
				break;
357
358
			case 'member_ip':
359
			case 'member_ip2':
360
				$type = 'inet';
361
				break;
362
363
			default:
364
				$type = 'string';
365
		}
366
367
		if (in_array($var, $knownInts))
368
			$type = 'int';
369
370
		elseif (in_array($var, $knownFloats))
371
			$type = 'float';
372
373
		// Doing an increment?
374
		if ($var == 'alerts' && ($val === '+' || $val === '-'))
375
		{
376
			include_once($sourcedir . '/Profile-Modify.php');
377
			if (is_array($members))
378
			{
379
				$val = 'CASE ';
380
				foreach ($members as $k => $v)
381
					$val .= 'WHEN id_member = ' . $v . ' THEN '. alert_count($v, true) . ' ';
382
383
				$val = $val . ' END';
384
				$type = 'raw';
385
			}
386
387
			else
388
				$val = alert_count($members, true);
389
		}
390
391
		elseif ($type == 'int' && ($val === '+' || $val === '-'))
392
		{
393
			$val = $var . ' ' . $val . ' 1';
394
			$type = 'raw';
395
		}
396
397
		// Ensure posts, instant_messages, and unread_messages don't overflow or underflow.
398
		if (in_array($var, array('posts', 'instant_messages', 'unread_messages')))
399
		{
400
			if (preg_match('~^' . $var . ' (\+ |- |\+ -)([\d]+)~', $val, $match))
401
			{
402
				if ($match[1] != '+ ')
403
					$val = 'CASE WHEN ' . $var . ' <= ' . abs($match[2]) . ' THEN 0 ELSE ' . $val . ' END';
404
405
				$type = 'raw';
406
			}
407
		}
408
409
		$setString .= ' ' . $var . ' = {' . $type . ':p_' . $var . '},';
410
		$parameters['p_' . $var] = $val;
411
	}
412
413
	$smcFunc['db_query']('', '
414
		UPDATE {db_prefix}members
415
		SET' . substr($setString, 0, -1) . '
416
		WHERE ' . $condition,
417
		$parameters
418
	);
419
420
	updateStats('postgroups', $members, array_keys($data));
421
422
	// Clear any caching?
423
	if (!empty($cache_enable) && $cache_enable >= 2 && !empty($members))
424
	{
425
		if (!is_array($members))
426
			$members = array($members);
427
428
		foreach ($members as $member)
429
		{
430
			if ($cache_enable >= 3)
431
			{
432
				cache_put_data('member_data-profile-' . $member, null, 120);
433
				cache_put_data('member_data-normal-' . $member, null, 120);
434
				cache_put_data('member_data-minimal-' . $member, null, 120);
435
			}
436
			cache_put_data('user_settings-' . $member, null, 60);
437
		}
438
	}
439
}
440
441
/**
442
 * Updates the settings table as well as $modSettings... only does one at a time if $update is true.
443
 *
444
 * - updates both the settings table and $modSettings array.
445
 * - all of changeArray's indexes and values are assumed to have escaped apostrophes (')!
446
 * - if a variable is already set to what you want to change it to, that
447
 *   variable will be skipped over; it would be unnecessary to reset.
448
 * - When use_update is true, UPDATEs will be used instead of REPLACE.
449
 * - when use_update is true, the value can be true or false to increment
450
 *  or decrement it, respectively.
451
 *
452
 * @param array $changeArray An array of info about what we're changing in 'setting' => 'value' format
453
 * @param bool $update Whether to use an UPDATE query instead of a REPLACE query
454
 */
455
function updateSettings($changeArray, $update = false)
456
{
457
	global $modSettings, $smcFunc;
458
459
	if (empty($changeArray) || !is_array($changeArray))
460
		return;
461
462
	$toRemove = array();
463
464
	// Go check if there is any setting to be removed.
465
	foreach ($changeArray as $k => $v)
466
		if ($v === null)
467
		{
468
			// Found some, remove them from the original array and add them to ours.
469
			unset($changeArray[$k]);
470
			$toRemove[] = $k;
471
		}
472
473
	// Proceed with the deletion.
474
	if (!empty($toRemove))
475
		$smcFunc['db_query']('', '
476
			DELETE FROM {db_prefix}settings
477
			WHERE variable IN ({array_string:remove})',
478
			array(
479
				'remove' => $toRemove,
480
			)
481
		);
482
483
	// In some cases, this may be better and faster, but for large sets we don't want so many UPDATEs.
484
	if ($update)
485
	{
486
		foreach ($changeArray as $variable => $value)
487
		{
488
			$smcFunc['db_query']('', '
489
				UPDATE {db_prefix}settings
490
				SET value = {' . ($value === false || $value === true ? 'raw' : 'string') . ':value}
491
				WHERE variable = {string:variable}',
492
				array(
493
					'value' => $value === true ? 'value + 1' : ($value === false ? 'value - 1' : $value),
494
					'variable' => $variable,
495
				)
496
			);
497
			$modSettings[$variable] = $value === true ? $modSettings[$variable] + 1 : ($value === false ? $modSettings[$variable] - 1 : $value);
498
		}
499
500
		// Clean out the cache and make sure the cobwebs are gone too.
501
		cache_put_data('modSettings', null, 90);
502
503
		return;
504
	}
505
506
	$replaceArray = array();
507
	foreach ($changeArray as $variable => $value)
508
	{
509
		// Don't bother if it's already like that ;).
510
		if (isset($modSettings[$variable]) && $modSettings[$variable] == $value)
511
			continue;
512
		// If the variable isn't set, but would only be set to nothing'ness, then don't bother setting it.
513
		elseif (!isset($modSettings[$variable]) && empty($value))
514
			continue;
515
516
		$replaceArray[] = array($variable, $value);
517
518
		$modSettings[$variable] = $value;
519
	}
520
521
	if (empty($replaceArray))
522
		return;
523
524
	$smcFunc['db_insert']('replace',
525
		'{db_prefix}settings',
526
		array('variable' => 'string-255', 'value' => 'string-65534'),
527
		$replaceArray,
528
		array('variable')
529
	);
530
531
	// Kill the cache - it needs redoing now, but we won't bother ourselves with that here.
532
	cache_put_data('modSettings', null, 90);
533
}
534
535
/**
536
 * Constructs a page list.
537
 *
538
 * - builds the page list, e.g. 1 ... 6 7 [8] 9 10 ... 15.
539
 * - flexible_start causes it to use "url.page" instead of "url;start=page".
540
 * - very importantly, cleans up the start value passed, and forces it to
541
 *   be a multiple of num_per_page.
542
 * - checks that start is not more than max_value.
543
 * - base_url should be the URL without any start parameter on it.
544
 * - uses the compactTopicPagesEnable and compactTopicPagesContiguous
545
 *   settings to decide how to display the menu.
546
 *
547
 * an example is available near the function definition.
548
 * $pageindex = constructPageIndex($scripturl . '?board=' . $board, $_REQUEST['start'], $num_messages, $maxindex, true);
549
 *
550
 * @param string $base_url The basic URL to be used for each link.
551
 * @param int &$start The start position, by reference. If this is not a multiple of the number of items per page, it is sanitized to be so and the value will persist upon the function's return.
552
 * @param int $max_value The total number of items you are paginating for.
553
 * @param int $num_per_page The number of items to be displayed on a given page. $start will be forced to be a multiple of this value.
554
 * @param bool $flexible_start Whether a ;start=x component should be introduced into the URL automatically (see above)
555
 * @param bool $show_prevnext Whether the Previous and Next links should be shown (should be on only when navigating the list)
556
 *
557
 * @return string The complete HTML of the page index that was requested, formatted by the template.
558
 */
559
function constructPageIndex($base_url, &$start, $max_value, $num_per_page, $flexible_start = false, $show_prevnext = true)
560
{
561
	global $modSettings, $context, $smcFunc, $settings, $txt;
562
563
	// Save whether $start was less than 0 or not.
564
	$start = (int) $start;
565
	$start_invalid = $start < 0;
566
567
	// Make sure $start is a proper variable - not less than 0.
568
	if ($start_invalid)
569
		$start = 0;
570
	// Not greater than the upper bound.
571
	elseif ($start >= $max_value)
572
		$start = max(0, (int) $max_value - (((int) $max_value % (int) $num_per_page) == 0 ? $num_per_page : ((int) $max_value % (int) $num_per_page)));
573
	// And it has to be a multiple of $num_per_page!
574
	else
575
		$start = max(0, (int) $start - ((int) $start % (int) $num_per_page));
576
577
	$context['current_page'] = $start / $num_per_page;
578
579
	// Define some default page index settings if we don't already have it...
580
	if (!isset($settings['page_index']))
581
	{
582
		// This defines the formatting for the page indexes used throughout the forum.
583
		$settings['page_index'] = array(
584
			'extra_before' => '<span class="pages">' . $txt['pages'] . '</span>',
585
			'previous_page' => '<span class="main_icons previous_page"></span>',
586
			'current_page' => '<span class="current_page">%1$d</span> ',
587
			'page' => '<a class="nav_page" href="{URL}">%2$s</a> ',
588
			'expand_pages' => '<span class="expand_pages" onclick="expandPages(this, {LINK}, {FIRST_PAGE}, {LAST_PAGE}, {PER_PAGE});"> ... </span>',
589
			'next_page' => '<span class="main_icons next_page"></span>',
590
			'extra_after' => '',
591
		);
592
	}
593
594
	$base_link = strtr($settings['page_index']['page'], array('{URL}' => $flexible_start ? $base_url : strtr($base_url, array('%' => '%%')) . ';start=%1$d'));
595
	$pageindex = $settings['page_index']['extra_before'];
596
597
	// Compact pages is off or on?
598
	if (empty($modSettings['compactTopicPagesEnable']))
599
	{
600
		// Show the left arrow.
601
		$pageindex .= $start == 0 ? ' ' : sprintf($base_link, $start - $num_per_page, $settings['page_index']['previous_page']);
602
603
		// Show all the pages.
604
		$display_page = 1;
605
		for ($counter = 0; $counter < $max_value; $counter += $num_per_page)
606
			$pageindex .= $start == $counter && !$start_invalid ? sprintf($settings['page_index']['current_page'], $display_page++) : sprintf($base_link, $counter, $display_page++);
607
608
		// Show the right arrow.
609
		$display_page = ($start + $num_per_page) > $max_value ? $max_value : ($start + $num_per_page);
610
		if ($start != $counter - $max_value && !$start_invalid)
611
			$pageindex .= $display_page > $counter - $num_per_page ? ' ' : sprintf($base_link, $display_page, $settings['page_index']['next_page']);
612
	}
613
	else
614
	{
615
		// If they didn't enter an odd value, pretend they did.
616
		$PageContiguous = (int) ($modSettings['compactTopicPagesContiguous'] - ($modSettings['compactTopicPagesContiguous'] % 2)) / 2;
617
618
		// Show the "prev page" link. (>prev page< 1 ... 6 7 [8] 9 10 ... 15 next page)
619
		if (!empty($start) && $show_prevnext)
620
			$pageindex .= sprintf($base_link, $start - $num_per_page, $settings['page_index']['previous_page']);
621
		else
622
			$pageindex .= '';
623
624
		// Show the first page. (prev page >1< ... 6 7 [8] 9 10 ... 15)
625
		if ($start > $num_per_page * $PageContiguous)
626
			$pageindex .= sprintf($base_link, 0, '1');
627
628
		// Show the ... after the first page.  (prev page 1 >...< 6 7 [8] 9 10 ... 15 next page)
629
		if ($start > $num_per_page * ($PageContiguous + 1))
630
			$pageindex .= strtr($settings['page_index']['expand_pages'], array(
631
				'{LINK}' => JavaScriptEscape($smcFunc['htmlspecialchars']($base_link)),
632
				'{FIRST_PAGE}' => $num_per_page,
633
				'{LAST_PAGE}' => $start - $num_per_page * $PageContiguous,
634
				'{PER_PAGE}' => $num_per_page,
635
			));
636
637
		// Show the pages before the current one. (prev page 1 ... >6 7< [8] 9 10 ... 15 next page)
638
		for ($nCont = $PageContiguous; $nCont >= 1; $nCont--)
639
			if ($start >= $num_per_page * $nCont)
640
			{
641
				$tmpStart = $start - $num_per_page * $nCont;
642
				$pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
643
			}
644
645
		// Show the current page. (prev page 1 ... 6 7 >[8]< 9 10 ... 15 next page)
646
		if (!$start_invalid)
647
			$pageindex .= sprintf($settings['page_index']['current_page'], $start / $num_per_page + 1);
648
		else
649
			$pageindex .= sprintf($base_link, $start, $start / $num_per_page + 1);
650
651
		// Show the pages after the current one... (prev page 1 ... 6 7 [8] >9 10< ... 15 next page)
652
		$tmpMaxPages = (int) (($max_value - 1) / $num_per_page) * $num_per_page;
653
		for ($nCont = 1; $nCont <= $PageContiguous; $nCont++)
654
			if ($start + $num_per_page * $nCont <= $tmpMaxPages)
655
			{
656
				$tmpStart = $start + $num_per_page * $nCont;
657
				$pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
658
			}
659
660
		// Show the '...' part near the end. (prev page 1 ... 6 7 [8] 9 10 >...< 15 next page)
661
		if ($start + $num_per_page * ($PageContiguous + 1) < $tmpMaxPages)
662
			$pageindex .= strtr($settings['page_index']['expand_pages'], array(
663
				'{LINK}' => JavaScriptEscape($smcFunc['htmlspecialchars']($base_link)),
664
				'{FIRST_PAGE}' => $start + $num_per_page * ($PageContiguous + 1),
665
				'{LAST_PAGE}' => $tmpMaxPages,
666
				'{PER_PAGE}' => $num_per_page,
667
			));
668
669
		// Show the last number in the list. (prev page 1 ... 6 7 [8] 9 10 ... >15<  next page)
670
		if ($start + $num_per_page * $PageContiguous < $tmpMaxPages)
671
			$pageindex .= sprintf($base_link, $tmpMaxPages, $tmpMaxPages / $num_per_page + 1);
672
673
		// Show the "next page" link. (prev page 1 ... 6 7 [8] 9 10 ... 15 >next page<)
674
		if ($start != $tmpMaxPages && $show_prevnext)
675
			$pageindex .= sprintf($base_link, $start + $num_per_page, $settings['page_index']['next_page']);
676
	}
677
	$pageindex .= $settings['page_index']['extra_after'];
678
679
	return $pageindex;
680
}
681
682
/**
683
 * - Formats a number.
684
 * - uses the format of number_format to decide how to format the number.
685
 *   for example, it might display "1 234,50".
686
 * - caches the formatting data from the setting for optimization.
687
 *
688
 * @param float $number A number
689
 * @param bool|int $override_decimal_count If set, will use the specified number of decimal places. Otherwise it's automatically determined
690
 * @return string A formatted number
691
 */
692
function comma_format($number, $override_decimal_count = false)
693
{
694
	global $txt;
695
	static $thousands_separator = null, $decimal_separator = null, $decimal_count = null;
696
697
	// Cache these values...
698
	if ($decimal_separator === null)
699
	{
700
		// Not set for whatever reason?
701
		if (empty($txt['number_format']) || preg_match('~^1([^\d]*)?234([^\d]*)(0*?)$~', $txt['number_format'], $matches) != 1)
702
			return $number;
703
704
		// Cache these each load...
705
		$thousands_separator = $matches[1];
706
		$decimal_separator = $matches[2];
707
		$decimal_count = strlen($matches[3]);
708
	}
709
710
	// Format the string with our friend, number_format.
711
	return number_format($number, (float) $number === $number ? ($override_decimal_count === false ? $decimal_count : $override_decimal_count) : 0, $decimal_separator, $thousands_separator);
0 ignored issues
show
Bug introduced by
It seems like (double)$number === $num...rride_decimal_count : 0 can also be of type true; however, parameter $decimals of number_format() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
3791
					// Nested tag with quoted value detected, use next end tag
3792
					$nested_tag_pos = strpos($message, $quoted == false ? ']' : '&quot;]', $pos1) + 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...
3793
			}
3794
3795
			$pos2 = strpos($message, $quoted == false ? ']' : '&quot;]', isset($nested_tag_pos) ? $nested_tag_pos : $pos1);
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

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

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

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

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

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

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

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

5258
		if (strpos(/** @scrutinizer ignore-type */ $test, 'not found') !== false)
Loading history...
5259
			$host = '';
5260
		// Invalid server option?
5261
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
5262
			updateSettings(array('host_to_dis' => 1));
5263
		// Maybe it found something, after all?
5264
		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

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

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

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

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

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

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

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

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

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

6478
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
6479
		// all others get html-ised
6480
		else
6481
			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

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

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

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

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

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

6657
	$normal_priority_tzids = array_diff(array_unique(array_merge(array_keys($tzid_metazones), /** @scrutinizer ignore-type */ timezone_identifiers_list())), $priority_tzids, $low_priority_tzids);
Loading history...
Bug introduced by
Are you sure the usage of timezone_identifiers_list() is correct as it seems to always return null.

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

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

}

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

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

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

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

6754
	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

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

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

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

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

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

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

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

}

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

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

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

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

7381
		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

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

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

7699
	$url = /** @scrutinizer ignore-type */ str_ireplace('https://', 'http://', $url) . '/';
Loading history...
7700
	$headers = @get_headers($url);
7701
	if ($headers === false)
7702
		return false;
7703
7704
	// Now to see if it came back https...
7705
	// First check for a redirect status code in first row (301, 302, 307)
7706
	if (strstr($headers[0], '301') === false && strstr($headers[0], '302') === false && strstr($headers[0], '307') === false)
7707
		return false;
7708
7709
	// Search for the location entry to confirm https
7710
	$result = false;
7711
	foreach ($headers as $header)
7712
	{
7713
		if (stristr($header, 'Location: https://') !== false)
7714
		{
7715
			$result = true;
7716
			break;
7717
		}
7718
	}
7719
	return $result;
7720
}
7721
7722
/**
7723
 * Build query_wanna_see_board and query_see_board for a userid
7724
 *
7725
 * Returns array with keys query_wanna_see_board and query_see_board
7726
 *
7727
 * @param int $userid of the user
7728
 */
7729
function build_query_board($userid)
7730
{
7731
	global $user_info, $modSettings, $smcFunc, $db_prefix;
7732
7733
	$query_part = array();
7734
7735
	// If we come from cron, we can't have a $user_info.
7736
	if (isset($user_info['id']) && $user_info['id'] == $userid && SMF != 'BACKGROUND')
7737
	{
7738
		$groups = $user_info['groups'];
7739
		$can_see_all_boards = $user_info['is_admin'] || $user_info['can_manage_boards'];
7740
		$ignoreboards = !empty($user_info['ignoreboards']) ? $user_info['ignoreboards'] : null;
7741
	}
7742
	else
7743
	{
7744
		$request = $smcFunc['db_query']('', '
7745
			SELECT mem.ignore_boards, mem.id_group, mem.additional_groups, mem.id_post_group
7746
			FROM {db_prefix}members AS mem
7747
			WHERE mem.id_member = {int:id_member}
7748
			LIMIT 1',
7749
			array(
7750
				'id_member' => $userid,
7751
			)
7752
		);
7753
7754
		$row = $smcFunc['db_fetch_assoc']($request);
7755
7756
		if (empty($row['additional_groups']))
7757
			$groups = array($row['id_group'], $row['id_post_group']);
7758
		else
7759
			$groups = array_merge(
7760
				array($row['id_group'], $row['id_post_group']),
7761
				explode(',', $row['additional_groups'])
7762
			);
7763
7764
		// Because history has proven that it is possible for groups to go bad - clean up in case.
7765
		foreach ($groups as $k => $v)
7766
			$groups[$k] = (int) $v;
7767
7768
		$can_see_all_boards = in_array(1, $groups) || (!empty($modSettings['board_manager_groups']) && count(array_intersect($groups, explode(',', $modSettings['board_manager_groups']))) > 0);
7769
7770
		$ignoreboards = !empty($row['ignore_boards']) && !empty($modSettings['allow_ignore_boards']) ? explode(',', $row['ignore_boards']) : array();
7771
	}
7772
7773
	// Just build this here, it makes it easier to change/use - administrators can see all boards.
7774
	if ($can_see_all_boards)
7775
		$query_part['query_see_board'] = '1=1';
7776
	// Otherwise just the groups in $user_info['groups'].
7777
	else
7778
	{
7779
		$query_part['query_see_board'] = '
7780
			EXISTS (
7781
				SELECT bpv.id_board
7782
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
7783
				WHERE bpv.id_group IN ('. implode(',', $groups) .')
7784
					AND bpv.deny = 0
7785
					AND bpv.id_board = b.id_board
7786
			)';
7787
7788
		if (!empty($modSettings['deny_boards_access']))
7789
			$query_part['query_see_board'] .= '
7790
			AND NOT EXISTS (
7791
				SELECT bpv.id_board
7792
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
7793
				WHERE bpv.id_group IN ( '. implode(',', $groups) .')
7794
					AND bpv.deny = 1
7795
					AND bpv.id_board = b.id_board
7796
			)';
7797
	}
7798
7799
	$query_part['query_see_message_board'] = str_replace('b.', 'm.', $query_part['query_see_board']);
7800
	$query_part['query_see_topic_board'] = str_replace('b.', 't.', $query_part['query_see_board']);
7801
7802
	// Build the list of boards they WANT to see.
7803
	// This will take the place of query_see_boards in certain spots, so it better include the boards they can see also
7804
7805
	// If they aren't ignoring any boards then they want to see all the boards they can see
7806
	if (empty($ignoreboards))
7807
	{
7808
		$query_part['query_wanna_see_board'] = $query_part['query_see_board'];
7809
		$query_part['query_wanna_see_message_board'] = $query_part['query_see_message_board'];
7810
		$query_part['query_wanna_see_topic_board'] = $query_part['query_see_topic_board'];
7811
	}
7812
	// Ok I guess they don't want to see all the boards
7813
	else
7814
	{
7815
		$query_part['query_wanna_see_board'] = '(' . $query_part['query_see_board'] . ' AND b.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7816
		$query_part['query_wanna_see_message_board'] = '(' . $query_part['query_see_message_board'] . ' AND m.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7817
		$query_part['query_wanna_see_topic_board'] = '(' . $query_part['query_see_topic_board'] . ' AND t.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7818
	}
7819
7820
	return $query_part;
7821
}
7822
7823
/**
7824
 * Check if the connection is using https.
7825
 *
7826
 * @return boolean true if connection used https
7827
 */
7828
function httpsOn()
7829
{
7830
	$secure = false;
7831
7832
	if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on')
7833
		$secure = true;
7834
	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...
7835
		$secure = true;
7836
7837
	return $secure;
7838
}
7839
7840
/**
7841
 * A wrapper for `parse_url($url)` that can handle URLs with international
7842
 * characters (a.k.a. IRIs)
7843
 *
7844
 * @param string $iri The IRI to parse.
7845
 * @param int $component Optional parameter to pass to parse_url().
7846
 * @return mixed Same as parse_url(), but with unmangled Unicode.
7847
 */
7848
function parse_iri($iri, $component = -1)
7849
{
7850
	$iri = preg_replace_callback(
7851
		'~[^\x00-\x7F\pZ\pC]|%~u',
7852
		function($matches)
7853
		{
7854
			return rawurlencode($matches[0]);
7855
		},
7856
		$iri
7857
	);
7858
7859
	$parsed = parse_url($iri, $component);
7860
7861
	if (is_array($parsed))
0 ignored issues
show
introduced by
The condition is_array($parsed) is always false.
Loading history...
7862
	{
7863
		foreach ($parsed as &$part)
7864
			$part = rawurldecode($part);
7865
	}
7866
	elseif (is_string($parsed))
0 ignored issues
show
introduced by
The condition is_string($parsed) is always true.
Loading history...
7867
		$parsed = rawurldecode($parsed);
7868
7869
	return $parsed;
7870
}
7871
7872
/**
7873
 * A wrapper for `filter_var($url, FILTER_VALIDATE_URL)` that can handle URLs
7874
 * with international characters (a.k.a. IRIs)
7875
 *
7876
 * @param string $iri The IRI to test.
7877
 * @param int $flags Optional flags to pass to filter_var()
7878
 * @return string|bool Either the original IRI, or false if the IRI was invalid.
7879
 */
7880
function validate_iri($iri, $flags = null)
7881
{
7882
	$url = iri_to_url($iri);
7883
7884
	// PHP 5 doesn't recognize IPv6 addresses in the URL host.
7885
	if (version_compare(phpversion(), '7.0.0', '<'))
7886
	{
7887
		$host = parse_url((strpos($url, '//') === 0 ? 'http:' : '') . $url, PHP_URL_HOST);
7888
7889
		if (strpos($host, '[') === 0 && strpos($host, ']') === strlen($host) - 1 && strpos($host, ':') !== false)
7890
			$url = str_replace($host, '127.0.0.1', $url);
7891
	}
7892
7893
	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

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

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

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

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

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