Passed
Pull Request — release-2.1 (#7179)
by Jon
05:54
created

smf_strftime()   F

Complexity

Conditions 40
Paths 2368

Size

Total Lines 291
Code Lines 182

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 40
eloc 182
nc 2368
nop 3
dl 0
loc 291
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file has all the main functions in it that relate to, well, everything.
5
 *
6
 * Simple Machines Forum (SMF)
7
 *
8
 * @package SMF
9
 * @author Simple Machines https://www.simplemachines.org
10
 * @copyright 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
	if (!isset($timestamp))
920
		$timestamp = time();
921
922
	if (!isset($tzid))
923
		$tzid = getUserTimezone();
924
925
	// In case this function is called before reloadSettings().
926
	if (!isset($smcFunc['strtoupper']))
927
	{
928
		if (function_exists('mb_strtoupper'))
929
		{
930
			$smcFunc['strtoupper'] = 'mb_strtoupper';
931
			$smcFunc['strtolower'] = 'mb_strtolower';
932
		}
933
		elseif (isset($sourcedir))
934
		{
935
			require_once($sourcedir . '/Subs-Charset.php');
936
			$smcFunc['strtoupper'] = 'utf8_strtoupper';
937
			$smcFunc['strtolower'] = 'utf8_strtolower';
938
		}
939
		else
940
		{
941
			$smcFunc['strtoupper'] = 'strtoupper';
942
			$smcFunc['strtolower'] = 'strtolower';
943
		}
944
	}
945
946
	// A couple substitutions to make life easier.
947
	$format = strtr($format, array(
948
		'%h' => '%b',
949
		'%r' => '%I:%M:%S %p',
950
	));
951
952
	$format_equivalents = array(
953
		// Day
954
		'a' => 'D',
955
		'A' => 'l',
956
		'e' => 'j', // Complex: sprintf to prepend whitespace.
957
		'd' => 'd',
958
		'j' => 'z', // Complex: must add one and then sprintf to prepend zeros.
959
		'u' => 'N',
960
		'w' => 'w',
961
		// Week
962
		'U' => 'z_w_0', // Complex: calculated from these other values.
963
		'V' => 'W',
964
		'W' => 'z_w_1', // Complex: calculated from these other values.
965
		// Month
966
		'b' => 'M',
967
		'B' => 'F',
968
		'm' => 'm',
969
		// Year
970
		'C' => 'Y', // Complex: Get 'Y' then truncate to first two digits.
971
		'g' => 'o', // Complex: Get 'o' then truncate to last two digits.
972
		'G' => 'o', // Complex: Get 'o' then sprintf to ensure four digits.
973
		'y' => 'y',
974
		'Y' => 'Y',
975
		// Time
976
		'H' => 'H',
977
		'k' => 'G',
978
		'I' => 'h',
979
		'l' => 'g',
980
		'M' => 'i',
981
		'p' => 'A',
982
		'P' => 'a',
983
		'R' => 'H:i',
984
		'S' => 's',
985
		'T' => 'H:i:s',
986
		'X' => 'H:i:s', // No direct equivalent possible, so use ISO format.
987
		'z' => 'O',
988
		'Z' => 'T',
989
		// Time and Date Stamps
990
		'c' => 'c', // No direct equivalent possible, so use ISO format.
991
		'D' => 'm/d/y',
992
		'F' => 'Y-m-d',
993
		's' => 'U',
994
		'x' => 'Y-m-d', // No direct equivalent possible, so use ISO format.
995
		// Miscellaneous
996
		'n' => "\n",
997
		't' => "\t",
998
		'%' => '%',
999
	);
1000
1001
	$parts = preg_split('/%(' . implode('|', array_keys($format_equivalents)) . ')/', $format, 0, PREG_SPLIT_DELIM_CAPTURE);
1002
1003
	$placeholders = array();
1004
	$complex = false;
1005
1006
	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...
1007
	{
1008
		// Parts that are not strftime formats.
1009
		if ($i % 2 === 0 || !isset($format_equivalents[$parts[$i]]))
1010
		{
1011
			if ($parts[$i] === '')
1012
				continue;
1013
1014
			$placeholder = "\xEE\x84\x80" . $i . "\xEE\x84\x81";
1015
1016
			$placeholders[$placeholder] = $parts[$i];
1017
			$parts[$i] = $placeholder;
1018
		}
1019
		// Parts that need localized strings.
1020
		elseif (in_array($parts[$i], array('a', 'A', 'b', 'B')))
1021
		{
1022
			switch ($parts[$i])
1023
			{
1024
				case 'a':
1025
					$min = 0;
1026
					$max = 6;
1027
					$key = 'days_short';
1028
					$f = 'w';
1029
					$placeholder_end = "\xEE\x84\x83";
1030
1031
					break;
1032
1033
				case 'A':
1034
					$min = 0;
1035
					$max = 6;
1036
					$key = 'days';
1037
					$f = 'w';
1038
					$placeholder_end = "\xEE\x84\x82";
1039
1040
					break;
1041
1042
				case 'b':
1043
					$min = 1;
1044
					$max = 12;
1045
					$key = 'months_short';
1046
					$f = 'n';
1047
					$placeholder_end = "\xEE\x84\x85";
1048
1049
					break;
1050
1051
				case 'B':
1052
					$min = 1;
1053
					$max = 12;
1054
					$key = 'months';
1055
					$f = 'n';
1056
					$placeholder_end = "\xEE\x84\x84";
1057
1058
					break;
1059
			}
1060
1061
			$placeholder = "\xEE\x84\x80" . $f . $placeholder_end;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $f does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $placeholder_end does not seem to be defined for all execution paths leading up to this point.
Loading history...
1062
1063
			// Check whether $txt contains all expected strings.
1064
			// If not, use English default.
1065
			$txt_strings_exist = true;
1066
			for ($num = $min; $num <= $max; $num++)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $min does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $max does not seem to be defined for all execution paths leading up to this point.
Loading history...
1067
			{
1068
				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...
1069
				{
1070
					$txt_strings_exist = false;
1071
					break;
1072
				}
1073
				else
1074
					$placeholders[str_replace($f, $num, $placeholder)] = $txt[$key][$num];
1075
			}
1076
1077
			$parts[$i] = $txt_strings_exist ? $placeholder : $format_equivalents[$parts[$i]];
1078
		}
1079
		elseif (in_array($parts[$i], array('p', 'P')))
1080
		{
1081
			if (!isset($txt['time_am']) || !isset($txt['time_pm']))
1082
				continue;
1083
1084
			$placeholder = "\xEE\x84\x90" . $format_equivalents[$parts[$i]] . "\xEE\x84\x91";
1085
1086
			switch ($parts[$i])
1087
			{
1088
				// Lower case
1089
				case 'p':
1090
					$placeholders[str_replace($format_equivalents[$parts[$i]], 'AM', $placeholder)] = $smcFunc['strtoupper']($txt['time_am']);
1091
					$placeholders[str_replace($format_equivalents[$parts[$i]], 'PM', $placeholder)] = $smcFunc['strtoupper']($txt['time_pm']);
1092
					break;
1093
1094
				// Upper case
1095
				case 'P':
1096
					$placeholders[str_replace($format_equivalents[$parts[$i]], 'am', $placeholder)] = $smcFunc['strtolower']($txt['time_am']);
1097
					$placeholders[str_replace($format_equivalents[$parts[$i]], 'pm', $placeholder)] = $smcFunc['strtolower']($txt['time_pm']);
1098
					break;
1099
			}
1100
1101
			$parts[$i] = $placeholder;
1102
		}
1103
		// Parts that will need further processing.
1104
		elseif (in_array($parts[$i], array('j', 'C', 'U', 'W', 'G', 'g', 'e', 'l')))
1105
		{
1106
			$complex = true;
1107
1108
			switch ($parts[$i])
1109
			{
1110
				case 'j':
1111
					$placeholder_end = "\xEE\x84\xA1";
1112
					break;
1113
1114
				case 'C':
1115
					$placeholder_end = "\xEE\x84\xA2";
1116
					break;
1117
1118
				case 'U':
1119
				case 'W':
1120
					$placeholder_end = "\xEE\x84\xA3";
1121
					break;
1122
1123
				case 'G':
1124
					$placeholder_end = "\xEE\x84\xA4";
1125
					break;
1126
1127
				case 'g':
1128
					$placeholder_end = "\xEE\x84\xA5";
1129
					break;
1130
1131
				case 'e':
1132
				case 'l':
1133
					$placeholder_end = "\xEE\x84\xA6";
1134
			}
1135
1136
			$parts[$i] = "\xEE\x84\xA0" . $format_equivalents[$parts[$i]] . $placeholder_end;
1137
		}
1138
		// Parts with simple equivalents.
1139
		else
1140
			$parts[$i] = $format_equivalents[$parts[$i]];
1141
	}
1142
1143
	// The main event.
1144
	$d = date_create('@' . $timestamp);
1145
1146
	if (($tz = @timezone_open($tzid)) === false)
1147
		$tz = timezone_open(date_default_timezone_get());
1148
1149
	date_timezone_set($d, $tz);
1150
1151
	$date_string = strtr(date_format($d, implode('', $parts)), $placeholders);
1152
1153
	// Deal with the complicated ones.
1154
	if ($complex)
0 ignored issues
show
introduced by
The condition $complex is always false.
Loading history...
1155
	{
1156
		$date_string = preg_replace_callback(
1157
			'/\xEE\x84\xA0([\d_]+)(\xEE\x84(?:[\xA1-\xAF]))/',
1158
			function ($matches)
1159
			{
1160
				switch ($matches[2])
1161
				{
1162
					// %j
1163
					case "\xEE\x84\xA1":
1164
						$replacement = sprintf('%03d', (int) $matches[1] + 1);
1165
						break;
1166
1167
					// %C
1168
					case "\xEE\x84\xA2":
1169
						$replacement = substr(sprintf('%04d', $matches[1]), 0, 2);
1170
						break;
1171
1172
					// %U and %W
1173
					case "\xEE\x84\xA3":
1174
						list($day_of_year, $day_of_week, $first_day) = explode('_', $matches[1]);
1175
						$replacement = sprintf('%02d', floor(((int) $day_of_year - (int) $day_of_week + (int) $first_day) / 7) + 1);
1176
						break;
1177
1178
					// %G
1179
					case "\xEE\x84\xA4":
1180
						$replacement = sprintf('%04d', $matches[1]);
1181
						break;
1182
1183
					// %g
1184
					case "\xEE\x84\xA5":
1185
						$replacement = substr(sprintf('%04d', $matches[1]), -2);
1186
						break;
1187
1188
					// %e and %l
1189
					case "\xEE\x84\xA6":
1190
						$replacement = sprintf('%2d', $matches[1]);
1191
						break;
1192
1193
					// Shouldn't happen, but just in case...
1194
					default:
1195
						$replacement = $matches[1];
1196
						break;
1197
				}
1198
1199
				return $replacement;
1200
			},
1201
			$date_string
1202
		);
1203
	}
1204
1205
	return $date_string;
1206
}
1207
1208
/**
1209
 * Replacement for gmstrftime() that is compatible with PHP 8.1+.
1210
 *
1211
 * Calls smf_strftime() with the $tzid parameter set to 'UTC'.
1212
 *
1213
 * @param string $format A strftime() format string.
1214
 * @param int|null $timestamp A Unix timestamp.
1215
 *     If null, defaults to the current time.
1216
 * @return string The formatted datetime string.
1217
 */
1218
function smf_gmstrftime(string $format, int $timestamp = null)
1219
{
1220
	return smf_strftime($format, $timestamp, 'UTC');
1221
}
1222
1223
/**
1224
 * Replaces special entities in strings with the real characters.
1225
 *
1226
 * Functionally equivalent to htmlspecialchars_decode(), except that this also
1227
 * replaces '&nbsp;' with a simple space character.
1228
 *
1229
 * @param string $string A string
1230
 * @return string The string without entities
1231
 */
1232
function un_htmlspecialchars($string)
1233
{
1234
	global $context;
1235
	static $translation = array();
1236
1237
	// Determine the character set... Default to UTF-8
1238
	if (empty($context['character_set']))
1239
		$charset = 'UTF-8';
1240
	// Use ISO-8859-1 in place of non-supported ISO-8859 charsets...
1241
	elseif (strpos($context['character_set'], 'ISO-8859-') !== false && !in_array($context['character_set'], array('ISO-8859-5', 'ISO-8859-15')))
1242
		$charset = 'ISO-8859-1';
1243
	else
1244
		$charset = $context['character_set'];
1245
1246
	if (empty($translation))
1247
		$translation = array_flip(get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES, $charset)) + array('&#039;' => '\'', '&#39;' => '\'', '&nbsp;' => ' ');
1248
1249
	return strtr($string, $translation);
1250
}
1251
1252
/**
1253
 * Replaces invalid characters with a substitute.
1254
 *
1255
 * !!! Warning !!! Setting $substitute to '' in order to delete invalid
1256
 * characters from the string can create unexpected security problems. See
1257
 * https://www.unicode.org/reports/tr36/#Deletion_of_Noncharacters for an
1258
 * explanation.
1259
 *
1260
 * @param string $string The string to sanitize.
1261
 * @param int $level Controls filtering of invisible formatting characters.
1262
 *      0: Allow valid formatting characters. Use for sanitizing text in posts.
1263
 *      1: Allow necessary formatting characters. Use for sanitizing usernames.
1264
 *      2: Disallow all formatting characters. Use for internal comparisions
1265
 *         only, such as in the word censor, search contexts, etc.
1266
 *      Default: 0.
1267
 * @param string|null $substitute Replacement string for the invalid characters.
1268
 *      If not set, the Unicode replacement character (U+FFFD) will be used
1269
 *      (or a fallback like "?" if necessary).
1270
 * @return string The sanitized string.
1271
 */
1272
function sanitize_chars($string, $level = 0, $substitute = null)
1273
{
1274
	global $context, $sourcedir;
1275
1276
	$string = (string) $string;
1277
	$level = min(max((int) $level, 0), 2);
1278
1279
	// What substitute character should we use?
1280
	if (isset($substitute))
1281
	{
1282
		$substitute = strval($substitute);
1283
	}
1284
	elseif (!empty($context['utf8']))
1285
	{
1286
		// Raw UTF-8 bytes for U+FFFD.
1287
		$substitute = "\xEF\xBF\xBD";
1288
	}
1289
	elseif (!empty($context['character_set']) && is_callable('mb_decode_numericentity'))
1290
	{
1291
		// Get whatever the default replacement character is for this encoding.
1292
		$substitute = mb_decode_numericentity('&#xFFFD;', array(0xFFFD,0xFFFD,0,0xFFFF), $context['character_set']);
1293
	}
1294
	else
1295
		$substitute = '?';
1296
1297
	// Fix any invalid byte sequences.
1298
	if (!empty($context['character_set']))
1299
	{
1300
		// For UTF-8, this preg_match test is much faster than mb_check_encoding.
1301
		$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']));
1302
1303
		if ($malformed)
1304
		{
1305
			// mb_convert_encoding will replace invalid byte sequences with our substitute.
1306
			if (is_callable('mb_convert_encoding'))
1307
			{
1308
				if (!is_callable('mb_ord'))
1309
					require_once($sourcedir . '/Subs-Compat.php');
1310
1311
				$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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

5238
		if (strpos(/** @scrutinizer ignore-type */ $test, 'not found') !== false)
Loading history...
5239
			$host = '';
5240
		// Invalid server option?
5241
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
5242
			updateSettings(array('host_to_dis' => 1));
5243
		// Maybe it found something, after all?
5244
		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

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

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

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

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

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

6284
			$temp_file = tempnam($cachedir, md5(/** @scrutinizer ignore-type */ $data));
Loading history...
6285
			file_put_contents($temp_file, $data);
6286
			$is_path = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $is_path is dead and can be removed.
Loading history...
6287
			$data = $temp_file;
6288
		}
6289
6290
		$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

6290
		$imagetype = @exif_imagetype(/** @scrutinizer ignore-type */ $data);
Loading history...
6291
6292
		if (isset($temp_file))
6293
			unlink($temp_file);
6294
6295
		// Unfortunately, this workaround only works for image files.
6296
		if ($imagetype !== false)
6297
			$mime_type = image_type_to_mime_type($imagetype);
6298
	}
6299
6300
	return $mime_type;
6301
}
6302
6303
/**
6304
 * Checks whether a file or data has the expected MIME type.
6305
 *
6306
 * @param string $data The data to check, or the path or URL of a file to check.
6307
 * @param string $type_pattern A regex pattern to match the acceptable MIME types.
6308
 * @param string $is_path If true, $data is a path or URL to a file.
6309
 * @return int 1 if the detected MIME type matches the pattern, 0 if it doesn't, or 2 if we can't check.
6310
 */
6311
function check_mime_type($data, $type_pattern, $is_path = false)
6312
{
6313
	// Get the MIME type.
6314
	$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

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

6441
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
6442
6443
	// remove left to right / right to left overrides
6444
	if ($num === 0x202D || $num === 0x202E)
6445
		return '';
6446
6447
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
6448
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
6449
		return '&#' . $num . ';';
6450
6451
	if (empty($context['utf8']))
6452
	{
6453
		// no control characters
6454
		if ($num < 0x20)
6455
			return '';
6456
		// text is text
6457
		elseif ($num < 0x80)
6458
			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

6458
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
6459
		// all others get html-ised
6460
		else
6461
			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

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

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

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

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

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

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

}

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

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

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

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

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

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

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

6734
	array_multisort($offsets, SORT_DESC, /** @scrutinizer ignore-type */ SORT_NUMERIC, $std_offsets, SORT_DESC, SORT_NUMERIC, $dst_types, SORT_ASC, $labels, SORT_ASC, $zones);
Loading history...
Comprehensibility Best Practice introduced by
The variable $zones does not seem to be defined for all execution paths leading up to this point.
Loading history...
Bug introduced by
SORT_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

6734
	array_multisort($offsets, SORT_DESC, SORT_NUMERIC, $std_offsets, SORT_DESC, SORT_NUMERIC, $dst_types, /** @scrutinizer ignore-type */ SORT_ASC, $labels, SORT_ASC, $zones);
Loading history...
Comprehensibility Best Practice introduced by
The variable $std_offsets does not seem to be defined for all execution paths leading up to this point.
Loading history...
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

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

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

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

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

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

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

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

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

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

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

8236
				$value = substr($value, 0, /** @scrutinizer ignore-type */ $param_max - strlen($key) - 5);
Loading history...
Bug introduced by
$value of type array is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

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

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