Passed
Pull Request — release-2.1 (#6409)
by
unknown
05:22
created

ip2range()   F

Complexity

Conditions 19
Paths 522

Size

Total Lines 72
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 43
c 0
b 0
f 0
nop 1
dl 0
loc 72
rs 1.0138
nc 522

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

712
	return number_format($number, /** @scrutinizer ignore-type */ (float) $number === $number ? ($override_decimal_count === false ? $decimal_count : $override_decimal_count) : 0, $decimal_separator, $thousands_separator);
Loading history...
713
}
714
715
/**
716
 * Format a time to make it look purdy.
717
 *
718
 * - returns a pretty formatted version of time based on the user's format in $user_info['time_format'].
719
 * - applies all necessary time offsets to the timestamp, unless offset_type is set.
720
 * - if todayMod is set and show_today was not not specified or true, an
721
 *   alternate format string is used to show the date with something to show it is "today" or "yesterday".
722
 * - performs localization (more than just strftime would do alone.)
723
 *
724
 * @param int $log_time A timestamp
725
 * @param bool|string $show_today Whether to show "Today"/"Yesterday" or just a date.
726
 *     If a string is specified, that is used to temporarily override the date format.
727
 * @param null|string $tzid Time zone to use when generating the formatted string.
728
 *     If empty, the user's time zone will be used.
729
 *     If set to 'forum', the value of $modSettings['default_timezone'] will be used.
730
 *     If set to a valid time zone identifier, that will be used.
731
 *     Otherwise, the value of date_default_timezone_get() will be used.
732
 * @return string A formatted time string
733
 */
734
function timeformat($log_time, $show_today = true, $tzid = null)
735
{
736
	global $context, $user_info, $txt, $modSettings;
737
	static $today;
738
739
	// Ensure required values are set
740
	$user_info['time_format'] = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %H:%M');
741
742
	// For backward compatibility, replace empty values with user's time zone
743
	// and replace 'forum' with forum's default time zone.
744
	$tzid = empty($tzid) ? getUserTimezone() : (($tzid === 'forum' || @timezone_open((string) $tzid) === false) ? $modSettings['default_timezone'] : (string) $tzid);
745
746
	// Today and Yesterday?
747
	$prefix = '';
748
	if ($modSettings['todayMod'] >= 1 && $show_today === true)
749
	{
750
		if (!isset($today[$tzid]))
751
			$today[$tzid] = date_format(date_create('today ' . $tzid), 'U');
752
753
		// Tomorrow? We don't support the future. ;)
754
		if ($log_time >= $today[$tzid] + 86400)
755
		{
756
			$prefix = '';
757
		}
758
		// Today.
759
		elseif ($log_time >= $today[$tzid])
760
		{
761
			$prefix = $txt['today'];
762
		}
763
		// Yesterday.
764
		elseif ($modSettings['todayMod'] > 1 && $log_time >= $today[$tzid] - 86400)
765
		{
766
			$prefix = $txt['yesterday'];
767
		}
768
	}
769
770
	// 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.
771
	$format = !is_bool($show_today) ? $show_today : $user_info['time_format'];
772
773
	$format = !empty($prefix) ? get_date_or_time_format('time', $format) : $format;
774
775
	// And now, the moment we've all be waiting for...
776
	return $prefix . smf_strftime($format, $log_time, $tzid);
777
}
778
779
/**
780
 * Gets a version of a strftime() format that only shows the date or time components
781
 *
782
 * @param string $type Either 'date' or 'time'.
783
 * @param string $format A strftime() format to process. Defaults to $user_info['time_format'].
784
 * @return string A strftime() format string
785
 */
786
function get_date_or_time_format($type = '', $format = '')
787
{
788
	global $user_info, $modSettings;
789
	static $formats;
790
791
	// If the format is invalid, fall back to defaults.
792
	if (strpos($format, '%') === false)
793
		$format = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %k:%M');
794
795
	$orig_format = $format;
796
797
	// Have we already done this?
798
	if (isset($formats[$orig_format][$type]))
799
		return $formats[$orig_format][$type];
800
801
	if ($type === 'date')
802
	{
803
		$specifications = array(
804
			// Day
805
			'%a' => '%a', '%A' => '%A', '%e' => '%e', '%d' => '%d', '%j' => '%j', '%u' => '%u', '%w' => '%w',
806
			// Week
807
			'%U' => '%U', '%V' => '%V', '%W' => '%W',
808
			// Month
809
			'%b' => '%b', '%B' => '%B', '%h' => '%h', '%m' => '%m',
810
			// Year
811
			'%C' => '%C', '%g' => '%g', '%G' => '%G', '%y' => '%y', '%Y' => '%Y',
812
			// Time
813
			'%H' => '', '%k' => '', '%I' => '', '%l' => '', '%M' => '', '%p' => '', '%P' => '',
814
			'%r' => '', '%R' => '', '%S' => '', '%T' => '', '%X' => '', '%z' => '', '%Z' => '',
815
			// Time and Date Stamps
816
			'%c' => '%x', '%D' => '%D', '%F' => '%F', '%s' => '%s', '%x' => '%x',
817
			// Miscellaneous
818
			'%n' => '', '%t' => '', '%%' => '%%',
819
		);
820
821
		$default_format = '%F';
822
	}
823
	elseif ($type === 'time')
824
	{
825
		$specifications = array(
826
			// Day
827
			'%a' => '', '%A' => '', '%e' => '', '%d' => '', '%j' => '', '%u' => '', '%w' => '',
828
			// Week
829
			'%U' => '', '%V' => '', '%W' => '',
830
			// Month
831
			'%b' => '', '%B' => '', '%h' => '', '%m' => '',
832
			// Year
833
			'%C' => '', '%g' => '', '%G' => '', '%y' => '', '%Y' => '',
834
			// Time
835
			'%H' => '%H', '%k' => '%k', '%I' => '%I', '%l' => '%l', '%M' => '%M', '%p' => '%p', '%P' => '%P',
836
			'%r' => '%r', '%R' => '%R', '%S' => '%S', '%T' => '%T', '%X' => '%X', '%z' => '%z', '%Z' => '%Z',
837
			// Time and Date Stamps
838
			'%c' => '%X', '%D' => '', '%F' => '', '%s' => '%s', '%x' => '',
839
			// Miscellaneous
840
			'%n' => '', '%t' => '', '%%' => '%%',
841
		);
842
843
		$default_format = '%k:%M';
844
	}
845
	// Invalid type requests just get the full format string.
846
	else
847
		return $format;
848
849
	// Separate the specifications we want from the ones we don't.
850
	$wanted = array_filter($specifications);
851
	$unwanted = array_diff(array_keys($specifications), $wanted);
852
853
	// First, make any necessary substitutions in the format.
854
	$format = strtr($format, $wanted);
855
856
	// Next, strip out any specifications and literal text that we don't want.
857
	$format_parts = preg_split('~%[' . (strtr(implode('', $unwanted), array('%' => ''))) . ']~u', $format);
858
859
	foreach ($format_parts as $p => $f)
860
	{
861
		if (strpos($f, '%') === false)
862
			unset($format_parts[$p]);
863
	}
864
865
	$format = implode('', $format_parts);
866
867
	// Finally, strip out any unwanted leftovers.
868
	// For info on the charcter classes used here, see https://www.php.net/manual/en/regexp.reference.unicode.php and https://www.regular-expressions.info/unicode.html
869
	$format = preg_replace(
870
		array(
871
			// Anything that isn't a specification, punctuation mark, or whitespace.
872
			'~(?<!%)\p{L}|[^\p{L}\p{P}\s]~u',
873
			// A series of punctuation marks (except %), possibly separated by whitespace.
874
			'~([^%\P{P}])(\s*)(?'.'>(\1|[^%\P{Po}])\s*(?!$))*~u',
875
			// Unwanted trailing punctuation and whitespace.
876
			'~(?'.'>([\p{Pd}\p{Ps}\p{Pi}\p{Pc}]|[^%\P{Po}])\s*)*$~u',
877
			// Unwanted opening punctuation and whitespace.
878
			'~^\s*(?'.'>([\p{Pd}\p{Pe}\p{Pf}\p{Pc}]|[^%\P{Po}])\s*)*~u',
879
		),
880
		array(
881
			'',
882
			'$1$2',
883
			'',
884
			'',
885
		),
886
		$format
887
	);
888
889
	// Gotta have something...
890
	if (empty($format))
891
		$format = $default_format;
892
893
	// Remember what we've done.
894
	$formats[$orig_format][$type] = trim($format);
895
896
	return $formats[$orig_format][$type];
897
}
898
899
/**
900
 * Replacement for strftime() that is compatible with PHP 8.1+.
901
 *
902
 * This does not use the system's strftime library or locale setting,
903
 * so results may vary in a few cases from the results of strftime():
904
 *
905
 *  - %a, %A, %b, %B, %p, %P: Output will use SMF's language strings
906
 *    to localize these values. If SMF's language strings have not
907
 *    been loaded, PHP's default English strings will be used.
908
 *
909
 *  - %c, %x, %X: Output will always use ISO format.
910
 *
911
 * @param string $format A strftime() format string.
912
 * @param int|null $timestamp A Unix timestamp.
913
 *     If null, defaults to the current time.
914
 * @param string|null $tzid Time zone identifier.
915
 *     If null, uses default time zone.
916
 * @return string The formatted datetime string.
917
 */
918
function smf_strftime(string $format, int $timestamp = null, string $tzid = null)
919
{
920
	global $txt, $smcFunc, $sourcedir;
921
922
	static $dates = array();
923
924
	// Set default values as necessary.
925
	if (!isset($timestamp))
926
		$timestamp = time();
927
928
	if (!isset($tzid))
929
		$tzid = date_default_timezone_get();
930
931
	// A few substitutions to make life easier.
932
	$format = strtr($format, array(
933
		'%h' => '%b',
934
		'%r' => '%I:%M:%S %p',
935
		'%R' => '%H:%M',
936
		'%T' => '%H:%M:%S',
937
		'%X' => '%H:%M:%S',
938
		'%D' => '%m/%d/%y',
939
		'%F' => '%Y-%m-%d',
940
		'%x' => '%Y-%m-%d',
941
	));
942
943
	// Avoid unnecessary repetition.
944
	if (isset($dates[$tzid . '_' . $timestamp]['results'][$format]))
945
		return $dates[$tzid . '_' . $timestamp]['results'][$format];
946
947
	// Ensure the TZID is valid.
948
	if (($tz = @timezone_open($tzid)) === false)
949
	{
950
		$tzid = date_default_timezone_get();
951
952
		// Check again now that we have a valid TZID.
953
		if (isset($dates[$tzid . '_' . $timestamp]['results'][$format]))
954
			return $dates[$tzid . '_' . $timestamp]['results'][$format];
955
956
		$tz = timezone_open($tzid);
957
	}
958
959
	// Create the DateTime object and set its time zone.
960
	if (!isset($dates[$tzid . '_' . $timestamp]['object']))
961
	{
962
		$dates[$tzid . '_' . $timestamp]['object'] = date_create('@' . $timestamp);
963
		date_timezone_set($dates[$tzid . '_' . $timestamp]['object'], $tz);
964
	}
965
966
	// In case this function is called before reloadSettings().
967
	if (!isset($smcFunc['strtoupper']))
968
	{
969
		if (function_exists('mb_strtoupper'))
970
		{
971
			$smcFunc['strtoupper'] = 'mb_strtoupper';
972
			$smcFunc['strtolower'] = 'mb_strtolower';
973
		}
974
		elseif (isset($sourcedir))
975
		{
976
			require_once($sourcedir . '/Subs-Charset.php');
977
			$smcFunc['strtoupper'] = 'utf8_strtoupper';
978
			$smcFunc['strtolower'] = 'utf8_strtolower';
979
		}
980
		else
981
		{
982
			$smcFunc['strtoupper'] = 'strtoupper';
983
			$smcFunc['strtolower'] = 'strtolower';
984
		}
985
	}
986
987
	$format_equivalents = array(
988
		// Day
989
		'a' => 'D', // Complex: prefer $txt strings if available.
990
		'A' => 'l', // Complex: prefer $txt strings if available.
991
		'e' => 'j', // Complex: sprintf to prepend whitespace.
992
		'd' => 'd',
993
		'j' => 'z', // Complex: must add one and then sprintf to prepend zeros.
994
		'u' => 'N',
995
		'w' => 'w',
996
		// Week
997
		'U' => 'z_w_0', // Complex: calculated from these other values.
998
		'V' => 'W',
999
		'W' => 'z_w_1', // Complex: calculated from these other values.
1000
		// Month
1001
		'b' => 'M', // Complex: prefer $txt strings if available.
1002
		'B' => 'F', // Complex: prefer $txt strings if available.
1003
		'm' => 'm',
1004
		// Year
1005
		'C' => 'Y', // Complex: Get 'Y' then truncate to first two digits.
1006
		'g' => 'o', // Complex: Get 'o' then truncate to last two digits.
1007
		'G' => 'o', // Complex: Get 'o' then sprintf to ensure four digits.
1008
		'y' => 'y',
1009
		'Y' => 'Y',
1010
		// Time
1011
		'H' => 'H',
1012
		'k' => 'G',
1013
		'I' => 'h',
1014
		'l' => 'g', // Complex: sprintf to prepend whitespace.
1015
		'M' => 'i',
1016
		'p' => 'A', // Complex: prefer $txt strings if available.
1017
		'P' => 'a', // Complex: prefer $txt strings if available.
1018
		'S' => 's',
1019
		'z' => 'O',
1020
		'Z' => 'T',
1021
		// Time and Date Stamps
1022
		'c' => 'c',
1023
		's' => 'U',
1024
		// Miscellaneous
1025
		'n' => "\n",
1026
		't' => "\t",
1027
		'%' => '%',
1028
	);
1029
1030
	// Translate from strftime format to DateTime format.
1031
	$parts = preg_split('/%(' . implode('|', array_keys($format_equivalents)) . ')/', $format, 0, PREG_SPLIT_DELIM_CAPTURE);
1032
1033
	$placeholders = array();
1034
	$complex = false;
1035
1036
	for ($i = 0; $i < count($parts); $i++)
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
1037
	{
1038
		// Parts that are not strftime formats.
1039
		if ($i % 2 === 0 || !isset($format_equivalents[$parts[$i]]))
1040
		{
1041
			if ($parts[$i] === '')
1042
				continue;
1043
1044
			$placeholder = "\xEE\x84\x80" . $i . "\xEE\x84\x81";
1045
1046
			$placeholders[$placeholder] = $parts[$i];
1047
			$parts[$i] = $placeholder;
1048
		}
1049
		// Parts that need localized strings.
1050
		elseif (in_array($parts[$i], array('a', 'A', 'b', 'B')))
1051
		{
1052
			switch ($parts[$i])
1053
			{
1054
				case 'a':
1055
					$min = 0;
1056
					$max = 6;
1057
					$key = 'days_short';
1058
					$f = 'w';
1059
					$placeholder_end = "\xEE\x84\x83";
1060
1061
					break;
1062
1063
				case 'A':
1064
					$min = 0;
1065
					$max = 6;
1066
					$key = 'days';
1067
					$f = 'w';
1068
					$placeholder_end = "\xEE\x84\x82";
1069
1070
					break;
1071
1072
				case 'b':
1073
					$min = 1;
1074
					$max = 12;
1075
					$key = 'months_short';
1076
					$f = 'n';
1077
					$placeholder_end = "\xEE\x84\x85";
1078
1079
					break;
1080
1081
				case 'B':
1082
					$min = 1;
1083
					$max = 12;
1084
					$key = 'months';
1085
					$f = 'n';
1086
					$placeholder_end = "\xEE\x84\x84";
1087
1088
					break;
1089
			}
1090
1091
			$placeholder = "\xEE\x84\x80" . $f . $placeholder_end;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $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...
1092
1093
			// Check whether $txt contains all expected strings.
1094
			// If not, use English default.
1095
			$txt_strings_exist = true;
1096
			for ($num = $min; $num <= $max; $num++)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $max does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $min does not seem to be defined for all execution paths leading up to this point.
Loading history...
1097
			{
1098
				if (!isset($txt[$key][$num]))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $key does not seem to be defined for all execution paths leading up to this point.
Loading history...
1099
				{
1100
					$txt_strings_exist = false;
1101
					break;
1102
				}
1103
				else
1104
					$placeholders[str_replace($f, $num, $placeholder)] = $txt[$key][$num];
1105
			}
1106
1107
			$parts[$i] = $txt_strings_exist ? $placeholder : $format_equivalents[$parts[$i]];
1108
		}
1109
		elseif (in_array($parts[$i], array('p', 'P')))
1110
		{
1111
			if (!isset($txt['time_am']) || !isset($txt['time_pm']))
1112
				continue;
1113
1114
			$placeholder = "\xEE\x84\x90" . $format_equivalents[$parts[$i]] . "\xEE\x84\x91";
1115
1116
			switch ($parts[$i])
1117
			{
1118
				// Lower case
1119
				case 'p':
1120
					$placeholders[str_replace($format_equivalents[$parts[$i]], 'AM', $placeholder)] = $smcFunc['strtoupper']($txt['time_am']);
1121
					$placeholders[str_replace($format_equivalents[$parts[$i]], 'PM', $placeholder)] = $smcFunc['strtoupper']($txt['time_pm']);
1122
					break;
1123
1124
				// Upper case
1125
				case 'P':
1126
					$placeholders[str_replace($format_equivalents[$parts[$i]], 'am', $placeholder)] = $smcFunc['strtolower']($txt['time_am']);
1127
					$placeholders[str_replace($format_equivalents[$parts[$i]], 'pm', $placeholder)] = $smcFunc['strtolower']($txt['time_pm']);
1128
					break;
1129
			}
1130
1131
			$parts[$i] = $placeholder;
1132
		}
1133
		// Parts that will need further processing.
1134
		elseif (in_array($parts[$i], array('j', 'C', 'U', 'W', 'G', 'g', 'e', 'l')))
1135
		{
1136
			$complex = true;
1137
1138
			switch ($parts[$i])
1139
			{
1140
				case 'j':
1141
					$placeholder_end = "\xEE\x84\xA1";
1142
					break;
1143
1144
				case 'C':
1145
					$placeholder_end = "\xEE\x84\xA2";
1146
					break;
1147
1148
				case 'U':
1149
				case 'W':
1150
					$placeholder_end = "\xEE\x84\xA3";
1151
					break;
1152
1153
				case 'G':
1154
					$placeholder_end = "\xEE\x84\xA4";
1155
					break;
1156
1157
				case 'g':
1158
					$placeholder_end = "\xEE\x84\xA5";
1159
					break;
1160
1161
				case 'e':
1162
				case 'l':
1163
					$placeholder_end = "\xEE\x84\xA6";
1164
			}
1165
1166
			$parts[$i] = "\xEE\x84\xA0" . $format_equivalents[$parts[$i]] . $placeholder_end;
1167
		}
1168
		// Parts with simple equivalents.
1169
		else
1170
			$parts[$i] = $format_equivalents[$parts[$i]];
1171
	}
1172
1173
	// The main event.
1174
	$dates[$tzid . '_' . $timestamp]['results'][$format] = strtr(date_format($dates[$tzid . '_' . $timestamp]['object'], implode('', $parts)), $placeholders);
1175
1176
	// Deal with the complicated ones.
1177
	if ($complex)
0 ignored issues
show
introduced by
The condition $complex is always false.
Loading history...
1178
	{
1179
		$dates[$tzid . '_' . $timestamp]['results'][$format] = preg_replace_callback(
1180
			'/\xEE\x84\xA0([\d_]+)(\xEE\x84(?:[\xA1-\xAF]))/',
1181
			function ($matches)
1182
			{
1183
				switch ($matches[2])
1184
				{
1185
					// %j
1186
					case "\xEE\x84\xA1":
1187
						$replacement = sprintf('%03d', (int) $matches[1] + 1);
1188
						break;
1189
1190
					// %C
1191
					case "\xEE\x84\xA2":
1192
						$replacement = substr(sprintf('%04d', $matches[1]), 0, 2);
1193
						break;
1194
1195
					// %U and %W
1196
					case "\xEE\x84\xA3":
1197
						list($day_of_year, $day_of_week, $first_day) = explode('_', $matches[1]);
1198
						$replacement = sprintf('%02d', floor(((int) $day_of_year - (int) $day_of_week + (int) $first_day) / 7) + 1);
1199
						break;
1200
1201
					// %G
1202
					case "\xEE\x84\xA4":
1203
						$replacement = sprintf('%04d', $matches[1]);
1204
						break;
1205
1206
					// %g
1207
					case "\xEE\x84\xA5":
1208
						$replacement = substr(sprintf('%04d', $matches[1]), -2);
1209
						break;
1210
1211
					// %e and %l
1212
					case "\xEE\x84\xA6":
1213
						$replacement = sprintf('%2d', $matches[1]);
1214
						break;
1215
1216
					// Shouldn't happen, but just in case...
1217
					default:
1218
						$replacement = $matches[1];
1219
						break;
1220
				}
1221
1222
				return $replacement;
1223
			},
1224
			$dates[$tzid . '_' . $timestamp]['results'][$format]
1225
		);
1226
	}
1227
1228
	return $dates[$tzid . '_' . $timestamp]['results'][$format];
1229
}
1230
1231
/**
1232
 * Replacement for gmstrftime() that is compatible with PHP 8.1+.
1233
 *
1234
 * Calls smf_strftime() with the $tzid parameter set to 'UTC'.
1235
 *
1236
 * @param string $format A strftime() format string.
1237
 * @param int|null $timestamp A Unix timestamp.
1238
 *     If null, defaults to the current time.
1239
 * @return string The formatted datetime string.
1240
 */
1241
function smf_gmstrftime(string $format, int $timestamp = null)
1242
{
1243
	return smf_strftime($format, $timestamp, 'UTC');
1244
}
1245
1246
/**
1247
 * Replaces special entities in strings with the real characters.
1248
 *
1249
 * Functionally equivalent to htmlspecialchars_decode(), except that this also
1250
 * replaces '&nbsp;' with a simple space character.
1251
 *
1252
 * @param string $string A string
1253
 * @return string The string without entities
1254
 */
1255
function un_htmlspecialchars($string)
1256
{
1257
	global $context;
1258
	static $translation = array();
1259
1260
	// Determine the character set... Default to UTF-8
1261
	$charset = 'UTF-8';
1262
1263
	if (empty($translation))
1264
		$translation = array_flip(get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES, $charset)) + array('&#039;' => '\'', '&#39;' => '\'', '&nbsp;' => ' ');
1265
1266
	return strtr($string, $translation);
1267
}
1268
1269
/**
1270
 * Replaces invalid characters with a substitute.
1271
 *
1272
 * !!! Warning !!! Setting $substitute to '' in order to delete invalid
1273
 * characters from the string can create unexpected security problems. See
1274
 * https://www.unicode.org/reports/tr36/#Deletion_of_Noncharacters for an
1275
 * explanation.
1276
 *
1277
 * @param string $string The string to sanitize.
1278
 * @param int $level Controls filtering of invisible formatting characters.
1279
 *      0: Allow valid formatting characters. Use for sanitizing text in posts.
1280
 *      1: Allow necessary formatting characters. Use for sanitizing usernames.
1281
 *      2: Disallow all formatting characters. Use for internal comparisions
1282
 *         only, such as in the word censor, search contexts, etc.
1283
 *      Default: 0.
1284
 * @param string|null $substitute Replacement string for the invalid characters.
1285
 *      If not set, the Unicode replacement character (U+FFFD) will be used
1286
 *      (or a fallback like "?" if necessary).
1287
 * @return string The sanitized string.
1288
 */
1289
function sanitize_chars($string, $level = 0, $substitute = null)
1290
{
1291
	global $context, $sourcedir;
1292
1293
	$string = (string) $string;
1294
	$level = min(max((int) $level, 0), 2);
1295
1296
	// What substitute character should we use?
1297
	if (isset($substitute))
1298
	{
1299
		$substitute = strval($substitute);
1300
	}
1301
	elseif (!empty($context['utf8']))
1302
	{
1303
		// Raw UTF-8 bytes for U+FFFD.
1304
		$substitute = "\xEF\xBF\xBD";
1305
	}
1306
	elseif (!empty($context['character_set']) && is_callable('mb_decode_numericentity'))
1307
	{
1308
		// Get whatever the default replacement character is for this encoding.
1309
		$substitute = mb_decode_numericentity('&#xFFFD;', array(0xFFFD,0xFFFD,0,0xFFFF), $context['character_set']);
1310
	}
1311
	else
1312
		$substitute = '?';
1313
1314
	// Fix any invalid byte sequences.
1315
	if (!empty($context['character_set']))
1316
	{
1317
		// For UTF-8, this preg_match test is much faster than mb_check_encoding.
1318
		$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']));
1319
1320
		if ($malformed)
1321
		{
1322
			// mb_convert_encoding will replace invalid byte sequences with our substitute.
1323
			if (is_callable('mb_convert_encoding'))
1324
			{
1325
				if (!is_callable('mb_ord'))
1326
					require_once($sourcedir . '/Subs-Compat.php');
1327
1328
				$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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

6434
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
6435
6436
	// remove left to right / right to left overrides
6437
	if ($num === 0x202D || $num === 0x202E)
6438
		return '';
6439
6440
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
6441
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
6442
		return '&#' . $num . ';';
6443
6444
	// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
6445
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
6446
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
6447
		return '';
6448
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
6449
	elseif ($num < 0x80)
6450
		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

6450
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
6451
	// <0x800 (2048)
6452
	elseif ($num < 0x800)
6453
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
6454
	// < 0x10000 (65536)
6455
	elseif ($num < 0x10000)
6456
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6457
	// <= 0x10FFFF (1114111)
6458
	else
6459
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6460
}
6461
6462
/**
6463
 * Converts html entities to utf8 equivalents
6464
 *
6465
 * Callback function for preg_replace_callback
6466
 * Uses capture group 1 in the supplied array
6467
 * Does basic checks to keep characters inside a viewable range.
6468
 *
6469
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
6470
 * @return string The fixed string
6471
 */
6472
function fixchar__callback($matches)
6473
{
6474
	if (!isset($matches[1]))
6475
		return '';
6476
6477
	$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

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

6485
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
6486
	// <0x800 (2048)
6487
	elseif ($num < 0x800)
6488
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
6489
	// < 0x10000 (65536)
6490
	elseif ($num < 0x10000)
6491
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6492
	// <= 0x10FFFF (1114111)
6493
	else
6494
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6495
}
6496
6497
/**
6498
 * Strips out invalid html entities, replaces others with html style &#123; codes
6499
 *
6500
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
6501
 * strpos, strlen, substr etc
6502
 *
6503
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
6504
 * @return string The fixed string
6505
 */
6506
function entity_fix__callback($matches)
6507
{
6508
	if (!isset($matches[2]))
6509
		return '';
6510
6511
	$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

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

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

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

6714
	array_multisort($offsets, /** @scrutinizer ignore-type */ SORT_DESC, SORT_NUMERIC, $std_offsets, SORT_DESC, SORT_NUMERIC, $dst_types, SORT_ASC, $labels, SORT_ASC, $zones);
Loading history...
Bug introduced by
SORT_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

6714
	array_multisort($offsets, SORT_DESC, /** @scrutinizer ignore-type */ SORT_NUMERIC, $std_offsets, SORT_DESC, SORT_NUMERIC, $dst_types, SORT_ASC, $labels, SORT_ASC, $zones);
Loading history...
Bug introduced by
SORT_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

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

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

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

}

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

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

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

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

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

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

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

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

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

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

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

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

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

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