Passed
Pull Request — release-2.1 (#7367)
by
unknown
05:45
created

obExit()   F

Complexity

Conditions 30
Paths > 20000

Size

Total Lines 92
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 30
eloc 48
nop 4
dl 0
loc 92
rs 0
c 0
b 0
f 0
nc 66305

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

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

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

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

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

1313
				$substitute_ord = $substitute === '' ? 'none' : mb_ord(/** @scrutinizer ignore-type */ $substitute, $context['character_set']);
Loading history...
1314
1315
				$mb_substitute_character = mb_substitute_character();
1316
				mb_substitute_character($substitute_ord);
1317
1318
				$string = mb_convert_encoding($string, $context['character_set'], $context['character_set']);
1319
1320
				mb_substitute_character($mb_substitute_character);
0 ignored issues
show
Bug introduced by
It seems like $mb_substitute_character can also be of type true; however, parameter $substitute_character of mb_substitute_character() does only seem to accept integer|null|string, maybe add an additional type check? ( Ignorable by Annotation )

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

1320
				mb_substitute_character(/** @scrutinizer ignore-type */ $mb_substitute_character);
Loading history...
1321
			}
1322
			else
1323
				return false;
1324
		}
1325
	}
1326
1327
	// Fix any weird vertical space characters.
1328
	$string = normalize_spaces($string, true);
0 ignored issues
show
Bug introduced by
It seems like $string can also be of type array; however, parameter $string of normalize_spaces() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

6474
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
6475
		// all others get html-ised
6476
		else
6477
			return '&#' . $matches[2] . ';';
0 ignored issues
show
Bug introduced by
Are you sure $matches[2] of type array can be used in concatenation? ( Ignorable by Annotation )

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

}

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

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

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

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

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

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

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

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

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

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

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

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

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