Passed
Pull Request — release-2.1 (#7032)
by Jon
04:13
created

template_header()   D

Complexity

Conditions 45

Size

Total Lines 139
Code Lines 79

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 45
eloc 79
c 1
b 1
f 0
nop 0
dl 0
loc 139
rs 4.1666

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

711
	return number_format($number, /** @scrutinizer ignore-type */ (float) $number === $number ? ($override_decimal_count === false ? $decimal_count : $override_decimal_count) : 0, $decimal_separator, $thousands_separator);
Loading history...
712
}
713
714
/**
715
 * Format a time to make it look purdy.
716
 *
717
 * - returns a pretty formatted version of time based on the user's format in $user_info['time_format'].
718
 * - applies all necessary time offsets to the timestamp, unless offset_type is set.
719
 * - if todayMod is set and show_today was not not specified or true, an
720
 *   alternate format string is used to show the date with something to show it is "today" or "yesterday".
721
 * - performs localization (more than just strftime would do alone.)
722
 *
723
 * @param int $log_time A timestamp
724
 * @param bool|string $show_today Whether to show "Today"/"Yesterday" or just a date. If a string is specified, that is used to temporarily override the date format.
725
 * @param bool|string $offset_type If false, uses both user time offset and forum offset. If 'forum', uses only the forum offset. Otherwise no offset is applied.
726
 * @param bool $process_safe Activate setlocale check for changes at runtime. Slower, but safer.
727
 * @return string A formatted timestamp
728
 */
729
function timeformat($log_time, $show_today = true, $offset_type = false, $process_safe = false)
730
{
731
	global $context, $user_info, $txt, $modSettings;
732
	static $non_twelve_hour, $locale, $now;
733
	static $unsupportedFormats, $finalizedFormats;
734
735
	$unsupportedFormatsWindows = array('z', 'Z');
736
737
	// Ensure required values are set
738
	$user_info['time_format'] = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %H:%M');
739
740
	// Offset the time.
741
	if (!$offset_type)
742
		$log_time = forum_time(true, $log_time);
743
	// Just the forum offset?
744
	elseif ($offset_type == 'forum')
745
		$log_time = forum_time(false, $log_time);
746
747
	// We can't have a negative date (on Windows, at least.)
748
	if ($log_time < 0)
749
		$log_time = 0;
750
751
	// Today and Yesterday?
752
	$prefix = '';
753
	if ($modSettings['todayMod'] >= 1 && $show_today === true)
754
	{
755
		$now_time = forum_time();
756
757
		if ($now_time - $log_time < (86400 * $modSettings['todayMod']))
758
		{
759
			$then = @getdate($log_time);
760
			$now = (!empty($now) ? $now : @getdate($now_time));
761
762
			// Same day of the year, same year.... Today!
763
			if ($then['yday'] == $now['yday'] && $then['year'] == $now['year'])
764
			{
765
				$prefix = $txt['today'];
766
			}
767
			// Day-of-year is one less and same year, or it's the first of the year and that's the last of the year...
768
			elseif ($modSettings['todayMod'] == '2' && (($then['yday'] == $now['yday'] - 1 && $then['year'] == $now['year']) || ($now['yday'] == 0 && $then['year'] == $now['year'] - 1) && $then['mon'] == 12 && $then['mday'] == 31))
769
			{
770
				$prefix = $txt['yesterday'];
771
			}
772
		}
773
	}
774
775
	// 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.
776
	$str = !is_bool($show_today) ? $show_today : $user_info['time_format'];
777
778
	// Use the cached formats if available
779
	if (is_null($finalizedFormats))
780
		$finalizedFormats = (array) cache_get_data('timeformatstrings', 86400);
781
782
	if (!isset($finalizedFormats[$str]) || !is_array($finalizedFormats[$str]))
783
		$finalizedFormats[$str] = array();
784
785
	// Make a supported version for this format if we don't already have one
786
	$format_type = !empty($prefix) ? 'time_only' : 'normal';
787
	if (empty($finalizedFormats[$str][$format_type]))
788
	{
789
		$timeformat = $format_type == 'time_only' ? get_date_or_time_format('time', $str) : $str;
790
791
		// Not all systems support all formats, and Windows fails altogether if unsupported ones are
792
		// used, so let's prevent that. Some substitutions go to the nearest reasonable fallback, some
793
		// turn into static strings, some (i.e. %a, %A, %b, %B, %p) have special handling below.
794
		$strftimeFormatSubstitutions = array(
795
			// Day
796
			'a' => '#txt_days_short_%w#', 'A' => '#txt_days_%w#', 'e' => '%d', 'd' => '&#37;d', 'j' => '&#37;j', 'u' => '%w', 'w' => '&#37;w',
797
			// Week
798
			'U' => '&#37;U', 'V' => '%U', 'W' => '%U',
799
			// Month
800
			'b' => '#txt_months_short_%m#', 'B' => '#txt_months_%m#', 'h' => '%b', 'm' => '&#37;m',
801
			// Year
802
			'C' => '&#37;C', 'g' => '%y', 'G' => '%Y', 'y' => '&#37;y', 'Y' => '&#37;Y',
803
			// Time
804
			'H' => '&#37;H', 'k' => '%H', 'I' => '%H', 'l' => '%I', 'M' => '&#37;M', 'p' => '&#37;p', 'P' => '%p',
805
			'r' => '%I:%M:%S %p', 'R' => '%H:%M', 'S' => '&#37;S', 'T' => '%H:%M:%S', 'X' => '%T', 'z' => '&#37;z', 'Z' => '&#37;Z',
806
			// Time and Date Stamps
807
			'c' => '%F %T', 'D' => '%m/%d/%y', 'F' => '%Y-%m-%d', 's' => '&#37;s', 'x' => '%F',
808
			// Miscellaneous
809
			'n' => "\n", 't' => "\t", '%' => '&#37;',
810
		);
811
812
		// No need to do this part again if we already did it once
813
		if (is_null($unsupportedFormats))
814
			$unsupportedFormats = (array) cache_get_data('unsupportedtimeformats', 86400);
815
		if (empty($unsupportedFormats))
816
		{
817
			foreach ($strftimeFormatSubstitutions as $format => $substitution)
818
			{
819
				// Avoid a crashing bug with PHP 7 on certain versions of Windows
820
				if ($context['server']['is_windows'] && in_array($format, $unsupportedFormatsWindows))
821
				{
822
					$unsupportedFormats[] = $format;
823
					continue;
824
				}
825
826
				$value = @strftime('%' . $format);
827
828
				// Windows will return false for unsupported formats
829
				// Other operating systems return the format string as a literal
830
				if ($value === false || $value === $format)
831
					$unsupportedFormats[] = $format;
832
			}
833
			cache_put_data('unsupportedtimeformats', $unsupportedFormats, 86400);
834
		}
835
836
		// Windows needs extra help if $timeformat contains something completely invalid, e.g. '%Q'
837
		if (DIRECTORY_SEPARATOR === '\\')
838
			$timeformat = preg_replace('~%(?!' . implode('|', array_keys($strftimeFormatSubstitutions)) . ')~', '&#37;', $timeformat);
839
840
		// Substitute unsupported formats with supported ones
841
		if (!empty($unsupportedFormats))
842
			while (preg_match('~%(' . implode('|', $unsupportedFormats) . ')~', $timeformat, $matches))
843
				$timeformat = str_replace($matches[0], $strftimeFormatSubstitutions[$matches[1]], $timeformat);
844
845
		// Remember this so we don't need to do it again
846
		$finalizedFormats[$str][$format_type] = $timeformat;
847
		cache_put_data('timeformatstrings', $finalizedFormats, 86400);
848
	}
849
850
	$timeformat = $finalizedFormats[$str][$format_type];
851
852
	// Windows requires a slightly different language code identifier (LCID).
853
	// https://msdn.microsoft.com/en-us/library/cc233982.aspx
854
	$lang_locale = $context['server']['is_windows'] ? strtr($txt['lang_locale'], '_', '-') : $txt['lang_locale'];
855
856
	// Make sure we are using the correct locale.
857
	if (!isset($locale) || ($process_safe === true && setlocale(LC_TIME, '0') != $locale))
858
		$locale = setlocale(LC_TIME, array($lang_locale . '.' . $modSettings['global_character_set'], $lang_locale . '.' . $txt['lang_character_set'], $lang_locale));
859
860
	// If the current locale is unsupported, we'll have to localize the hard way.
861
	if ($locale === false)
862
	{
863
		$timeformat = strtr($timeformat, array(
864
			'%a' => '#txt_days_short_%w#',
865
			'%A' => '#txt_days_%w#',
866
			'%b' => '#txt_months_short_%m#',
867
			'%B' => '#txt_months_%m#',
868
			'%p' => '&#37;p',
869
			'%P' => '&#37;p'
870
		));
871
	}
872
	// Just in case the locale doesn't support '%p' properly.
873
	// @todo Is this even necessary?
874
	else
875
	{
876
		if (!isset($non_twelve_hour) && strpos($timeformat, '%p') !== false)
877
			$non_twelve_hour = trim(strftime('%p')) === '';
878
879
		if (!empty($non_twelve_hour))
880
			$timeformat = strtr($timeformat, array(
881
				'%p' => '&#37;p',
882
				'%P' => '&#37;p'
883
			));
884
	}
885
886
	// And now, the moment we've all be waiting for...
887
	$timestring = strftime($timeformat, $log_time);
888
889
	// Do-it-yourself time localization.  Fun.
890
	if (strpos($timestring, '&#37;p') !== false)
891
		$timestring = str_replace('&#37;p', (strftime('%H', $log_time) < 12 ? $txt['time_am'] : $txt['time_pm']), $timestring);
892
	if (strpos($timestring, '#txt_') !== false)
893
	{
894
		if (strpos($timestring, '#txt_days_short_') !== false)
895
			$timestring = strtr($timestring, array(
896
				'#txt_days_short_0#' => $txt['days_short'][0],
897
				'#txt_days_short_1#' => $txt['days_short'][1],
898
				'#txt_days_short_2#' => $txt['days_short'][2],
899
				'#txt_days_short_3#' => $txt['days_short'][3],
900
				'#txt_days_short_4#' => $txt['days_short'][4],
901
				'#txt_days_short_5#' => $txt['days_short'][5],
902
				'#txt_days_short_6#' => $txt['days_short'][6],
903
			));
904
905
		if (strpos($timestring, '#txt_days_') !== false)
906
			$timestring = strtr($timestring, array(
907
				'#txt_days_0#' => $txt['days'][0],
908
				'#txt_days_1#' => $txt['days'][1],
909
				'#txt_days_2#' => $txt['days'][2],
910
				'#txt_days_3#' => $txt['days'][3],
911
				'#txt_days_4#' => $txt['days'][4],
912
				'#txt_days_5#' => $txt['days'][5],
913
				'#txt_days_6#' => $txt['days'][6],
914
			));
915
916
		if (strpos($timestring, '#txt_months_short_') !== false)
917
			$timestring = strtr($timestring, array(
918
				'#txt_months_short_01#' => $txt['months_short'][1],
919
				'#txt_months_short_02#' => $txt['months_short'][2],
920
				'#txt_months_short_03#' => $txt['months_short'][3],
921
				'#txt_months_short_04#' => $txt['months_short'][4],
922
				'#txt_months_short_05#' => $txt['months_short'][5],
923
				'#txt_months_short_06#' => $txt['months_short'][6],
924
				'#txt_months_short_07#' => $txt['months_short'][7],
925
				'#txt_months_short_08#' => $txt['months_short'][8],
926
				'#txt_months_short_09#' => $txt['months_short'][9],
927
				'#txt_months_short_10#' => $txt['months_short'][10],
928
				'#txt_months_short_11#' => $txt['months_short'][11],
929
				'#txt_months_short_12#' => $txt['months_short'][12],
930
			));
931
932
		if (strpos($timestring, '#txt_months_') !== false)
933
			$timestring = strtr($timestring, array(
934
				'#txt_months_01#' => $txt['months'][1],
935
				'#txt_months_02#' => $txt['months'][2],
936
				'#txt_months_03#' => $txt['months'][3],
937
				'#txt_months_04#' => $txt['months'][4],
938
				'#txt_months_05#' => $txt['months'][5],
939
				'#txt_months_06#' => $txt['months'][6],
940
				'#txt_months_07#' => $txt['months'][7],
941
				'#txt_months_08#' => $txt['months'][8],
942
				'#txt_months_09#' => $txt['months'][9],
943
				'#txt_months_10#' => $txt['months'][10],
944
				'#txt_months_11#' => $txt['months'][11],
945
				'#txt_months_12#' => $txt['months'][12],
946
			));
947
	}
948
949
	// Restore any literal percent characters, add the prefix, and we're done.
950
	return $prefix . str_replace('&#37;', '%', $timestring);
951
}
952
953
/**
954
 * Gets a version of a strftime() format that only shows the date or time components
955
 *
956
 * @param string $type Either 'date' or 'time'.
957
 * @param string $format A strftime() format to process. Defaults to $user_info['time_format'].
958
 * @return string A strftime() format string
959
 */
960
function get_date_or_time_format($type = '', $format = '')
961
{
962
	global $user_info, $modSettings;
963
	static $formats;
964
965
	// If the format is invalid, fall back to defaults.
966
	if (strpos($format, '%') === false)
967
		$format = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %k:%M');
968
969
	$orig_format = $format;
970
971
	// Have we already done this?
972
	if (isset($formats[$orig_format][$type]))
973
		return $formats[$orig_format][$type];
974
975
	if ($type === 'date')
976
	{
977
		$specifications = array(
978
			// Day
979
			'%a' => '%a', '%A' => '%A', '%e' => '%e', '%d' => '%d', '%j' => '%j', '%u' => '%u', '%w' => '%w',
980
			// Week
981
			'%U' => '%U', '%V' => '%V', '%W' => '%W',
982
			// Month
983
			'%b' => '%b', '%B' => '%B', '%h' => '%h', '%m' => '%m',
984
			// Year
985
			'%C' => '%C', '%g' => '%g', '%G' => '%G', '%y' => '%y', '%Y' => '%Y',
986
			// Time
987
			'%H' => '', '%k' => '', '%I' => '', '%l' => '', '%M' => '', '%p' => '', '%P' => '',
988
			'%r' => '', '%R' => '', '%S' => '', '%T' => '', '%X' => '', '%z' => '', '%Z' => '',
989
			// Time and Date Stamps
990
			'%c' => '%x', '%D' => '%D', '%F' => '%F', '%s' => '%s', '%x' => '%x',
991
			// Miscellaneous
992
			'%n' => '', '%t' => '', '%%' => '%%',
993
		);
994
995
		$default_format = '%F';
996
	}
997
	elseif ($type === 'time')
998
	{
999
		$specifications = array(
1000
			// Day
1001
			'%a' => '', '%A' => '', '%e' => '', '%d' => '', '%j' => '', '%u' => '', '%w' => '',
1002
			// Week
1003
			'%U' => '', '%V' => '', '%W' => '',
1004
			// Month
1005
			'%b' => '', '%B' => '', '%h' => '', '%m' => '',
1006
			// Year
1007
			'%C' => '', '%g' => '', '%G' => '', '%y' => '', '%Y' => '',
1008
			// Time
1009
			'%H' => '%H', '%k' => '%k', '%I' => '%I', '%l' => '%l', '%M' => '%M', '%p' => '%p', '%P' => '%P',
1010
			'%r' => '%r', '%R' => '%R', '%S' => '%S', '%T' => '%T', '%X' => '%X', '%z' => '%z', '%Z' => '%Z',
1011
			// Time and Date Stamps
1012
			'%c' => '%X', '%D' => '', '%F' => '', '%s' => '%s', '%x' => '',
1013
			// Miscellaneous
1014
			'%n' => '', '%t' => '', '%%' => '%%',
1015
		);
1016
1017
		$default_format = '%k:%M';
1018
	}
1019
	// Invalid type requests just get the full format string.
1020
	else
1021
		return $format;
1022
1023
	// Separate the specifications we want from the ones we don't.
1024
	$wanted = array_filter($specifications);
1025
	$unwanted = array_diff(array_keys($specifications), $wanted);
1026
1027
	// First, make any necessary substitutions in the format.
1028
	$format = strtr($format, $wanted);
1029
1030
	// Next, strip out any specifications and literal text that we don't want.
1031
	$format_parts = preg_split('~%[' . (strtr(implode('', $unwanted), array('%' => ''))) . ']~u', $format);
1032
1033
	foreach ($format_parts as $p => $f)
1034
	{
1035
		if (strpos($f, '%') === false)
1036
			unset($format_parts[$p]);
1037
	}
1038
1039
	$format = implode('', $format_parts);
1040
1041
	// Finally, strip out any unwanted leftovers.
1042
	// 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
1043
	$format = preg_replace(
1044
		array(
1045
			// Anything that isn't a specification, punctuation mark, or whitespace.
1046
			'~(?<!%)\p{L}|[^\p{L}\p{P}\s]~u',
1047
			// A series of punctuation marks (except %), possibly separated by whitespace.
1048
			'~([^%\P{P}])(\s*)(?'.'>(\1|[^%\P{Po}])\s*(?!$))*~u',
1049
			// Unwanted trailing punctuation and whitespace.
1050
			'~(?'.'>([\p{Pd}\p{Ps}\p{Pi}\p{Pc}]|[^%\P{Po}])\s*)*$~u',
1051
			// Unwanted opening punctuation and whitespace.
1052
			'~^\s*(?'.'>([\p{Pd}\p{Pe}\p{Pf}\p{Pc}]|[^%\P{Po}])\s*)*~u',
1053
		),
1054
		array(
1055
			'',
1056
			'$1$2',
1057
			'',
1058
			'',
1059
		),
1060
		$format
1061
	);
1062
1063
	// Gotta have something...
1064
	if (empty($format))
1065
		$format = $default_format;
1066
1067
	// Remember what we've done.
1068
	$formats[$orig_format][$type] = trim($format);
1069
1070
	return $formats[$orig_format][$type];
1071
}
1072
1073
/**
1074
 * Replaces special entities in strings with the real characters.
1075
 *
1076
 * Functionally equivalent to htmlspecialchars_decode(), except that this also
1077
 * replaces '&nbsp;' with a simple space character.
1078
 *
1079
 * @param string $string A string
1080
 * @return string The string without entities
1081
 */
1082
function un_htmlspecialchars($string)
1083
{
1084
	global $context;
1085
	static $translation = array();
1086
1087
	// Determine the character set... Default to UTF-8
1088
	if (empty($context['character_set']))
1089
		$charset = 'UTF-8';
1090
	// Use ISO-8859-1 in place of non-supported ISO-8859 charsets...
1091
	elseif (strpos($context['character_set'], 'ISO-8859-') !== false && !in_array($context['character_set'], array('ISO-8859-5', 'ISO-8859-15')))
1092
		$charset = 'ISO-8859-1';
1093
	else
1094
		$charset = $context['character_set'];
1095
1096
	if (empty($translation))
1097
		$translation = array_flip(get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES, $charset)) + array('&#039;' => '\'', '&#39;' => '\'', '&nbsp;' => ' ');
1098
1099
	return strtr($string, $translation);
1100
}
1101
1102
/**
1103
 * Shorten a subject + internationalization concerns.
1104
 *
1105
 * - shortens a subject so that it is either shorter than length, or that length plus an ellipsis.
1106
 * - respects internationalization characters and entities as one character.
1107
 * - avoids trailing entities.
1108
 * - returns the shortened string.
1109
 *
1110
 * @param string $subject The subject
1111
 * @param int $len How many characters to limit it to
1112
 * @return string The shortened subject - either the entire subject (if it's <= $len) or the subject shortened to $len characters with "..." appended
1113
 */
1114
function shorten_subject($subject, $len)
1115
{
1116
	global $smcFunc;
1117
1118
	// It was already short enough!
1119
	if ($smcFunc['strlen']($subject) <= $len)
1120
		return $subject;
1121
1122
	// Shorten it by the length it was too long, and strip off junk from the end.
1123
	return $smcFunc['substr']($subject, 0, $len) . '...';
1124
}
1125
1126
/**
1127
 * Gets the current time with offset.
1128
 *
1129
 * - always applies the offset in the time_offset setting.
1130
 *
1131
 * @param bool $use_user_offset Whether to apply the user's offset as well
1132
 * @param int $timestamp A timestamp (null to use current time)
1133
 * @return int Seconds since the unix epoch, with forum time offset and (optionally) user time offset applied
1134
 */
1135
function forum_time($use_user_offset = true, $timestamp = null)
1136
{
1137
	global $user_info, $modSettings;
1138
1139
	// Ensure required values are set
1140
	$modSettings['time_offset'] = !empty($modSettings['time_offset']) ? $modSettings['time_offset'] : 0;
1141
	$user_info['time_offset'] = !empty($user_info['time_offset']) ? $user_info['time_offset'] : 0;
1142
1143
	if ($timestamp === null)
1144
		$timestamp = time();
1145
	elseif ($timestamp == 0)
1146
		return 0;
1147
1148
	return $timestamp + ($modSettings['time_offset'] + ($use_user_offset ? $user_info['time_offset'] : 0)) * 3600;
1149
}
1150
1151
/**
1152
 * Calculates all the possible permutations (orders) of array.
1153
 * should not be called on huge arrays (bigger than like 10 elements.)
1154
 * returns an array containing each permutation.
1155
 *
1156
 * @deprecated since 2.1
1157
 * @param array $array An array
1158
 * @return array An array containing each permutation
1159
 */
1160
function permute($array)
1161
{
1162
	$orders = array($array);
1163
1164
	$n = count($array);
1165
	$p = range(0, $n);
1166
	for ($i = 1; $i < $n; null)
1167
	{
1168
		$p[$i]--;
1169
		$j = $i % 2 != 0 ? $p[$i] : 0;
1170
1171
		$temp = $array[$i];
1172
		$array[$i] = $array[$j];
1173
		$array[$j] = $temp;
1174
1175
		for ($i = 1; $p[$i] == 0; $i++)
1176
			$p[$i] = 1;
1177
1178
		$orders[] = $array;
1179
	}
1180
1181
	return $orders;
1182
}
1183
1184
/**
1185
 * Parse bulletin board code in a string, as well as smileys optionally.
1186
 *
1187
 * - only parses bbc tags which are not disabled in disabledBBC.
1188
 * - handles basic HTML, if enablePostHTML is on.
1189
 * - caches the from/to replace regular expressions so as not to reload them every time a string is parsed.
1190
 * - only parses smileys if smileys is true.
1191
 * - does nothing if the enableBBC setting is off.
1192
 * - uses the cache_id as a unique identifier to facilitate any caching it may do.
1193
 * - returns the modified message.
1194
 *
1195
 * @param string|bool $message The message.
1196
 *		When a empty string, nothing is done.
1197
 *		When false we provide a list of BBC codes available.
1198
 *		When a string, the message is parsed and bbc handled.
1199
 * @param bool $smileys Whether to parse smileys as well
1200
 * @param string $cache_id The cache ID
1201
 * @param array $parse_tags If set, only parses these tags rather than all of them
1202
 * @return string The parsed message
1203
 */
1204
function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = array())
1205
{
1206
	global $smcFunc, $txt, $scripturl, $context, $modSettings, $user_info, $sourcedir, $cache_enable;
1207
	static $bbc_lang_locales = array(), $itemcodes = array(), $no_autolink_tags = array();
1208
	static $disabled, $alltags_regex = '', $param_regexes = array(), $url_regex = '';
1209
1210
	// Don't waste cycles
1211
	if ($message === '')
1212
		return '';
1213
1214
	// Just in case it wasn't determined yet whether UTF-8 is enabled.
1215
	if (!isset($context['utf8']))
1216
		$context['utf8'] = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8';
1217
1218
	// Clean up any cut/paste issues we may have
1219
	$message = sanitizeMSCutPaste($message);
1220
1221
	// If the load average is too high, don't parse the BBC.
1222
	if (!empty($context['load_average']) && !empty($modSettings['bbc']) && $context['load_average'] >= $modSettings['bbc'])
1223
	{
1224
		$context['disabled_parse_bbc'] = true;
1225
		return $message;
1226
	}
1227
1228
	if ($smileys !== null && ($smileys == '1' || $smileys == '0'))
1229
		$smileys = (bool) $smileys;
1230
1231
	if (empty($modSettings['enableBBC']) && $message !== false)
1232
	{
1233
		if ($smileys === true)
1234
			parsesmileys($message);
1235
1236
		return $message;
1237
	}
1238
1239
	// If we already have a version of the BBCodes for the current language, use that. Otherwise, make one.
1240
	if (!empty($bbc_lang_locales[$txt['lang_locale']]))
1241
		$bbc_codes = $bbc_lang_locales[$txt['lang_locale']];
1242
	else
1243
		$bbc_codes = array();
1244
1245
	// If we are not doing every tag then we don't cache this run.
1246
	if (!empty($parse_tags))
1247
		$bbc_codes = array();
1248
1249
	// Ensure $modSettings['tld_regex'] contains a valid regex for the autolinker
1250
	if (!empty($modSettings['autoLinkUrls']))
1251
		set_tld_regex();
1252
1253
	// Allow mods access before entering the main parse_bbc loop
1254
	call_integration_hook('integrate_pre_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
1255
1256
	// Sift out the bbc for a performance improvement.
1257
	if (empty($bbc_codes) || $message === false || !empty($parse_tags))
1258
	{
1259
		if (!empty($modSettings['disabledBBC']))
1260
		{
1261
			$disabled = array();
1262
1263
			$temp = explode(',', strtolower($modSettings['disabledBBC']));
1264
1265
			foreach ($temp as $tag)
1266
				$disabled[trim($tag)] = true;
1267
1268
			if (in_array('color', $disabled))
1269
				$disabled = array_merge($disabled, array(
1270
					'black' => true,
1271
					'white' => true,
1272
					'red' => true,
1273
					'green' => true,
1274
					'blue' => true,
1275
					)
1276
				);
1277
		}
1278
1279
		// The YouTube bbc needs this for its origin parameter
1280
		$scripturl_parts = parse_url($scripturl);
1281
		$hosturl = $scripturl_parts['scheme'] . '://' . $scripturl_parts['host'];
1282
1283
		/* The following bbc are formatted as an array, with keys as follows:
1284
1285
			tag: the tag's name - should be lowercase!
1286
1287
			type: one of...
1288
				- (missing): [tag]parsed content[/tag]
1289
				- unparsed_equals: [tag=xyz]parsed content[/tag]
1290
				- parsed_equals: [tag=parsed data]parsed content[/tag]
1291
				- unparsed_content: [tag]unparsed content[/tag]
1292
				- closed: [tag], [tag/], [tag /]
1293
				- unparsed_commas: [tag=1,2,3]parsed content[/tag]
1294
				- unparsed_commas_content: [tag=1,2,3]unparsed content[/tag]
1295
				- unparsed_equals_content: [tag=...]unparsed content[/tag]
1296
1297
			parameters: an optional array of parameters, for the form
1298
			  [tag abc=123]content[/tag].  The array is an associative array
1299
			  where the keys are the parameter names, and the values are an
1300
			  array which may contain the following:
1301
				- match: a regular expression to validate and match the value.
1302
				- quoted: true if the value should be quoted.
1303
				- validate: callback to evaluate on the data, which is $data.
1304
				- value: a string in which to replace $1 with the data.
1305
					Either value or validate may be used, not both.
1306
				- optional: true if the parameter is optional.
1307
				- default: a default value for missing optional parameters.
1308
1309
			test: a regular expression to test immediately after the tag's
1310
			  '=', ' ' or ']'.  Typically, should have a \] at the end.
1311
			  Optional.
1312
1313
			content: only available for unparsed_content, closed,
1314
			  unparsed_commas_content, and unparsed_equals_content.
1315
			  $1 is replaced with the content of the tag.  Parameters
1316
			  are replaced in the form {param}.  For unparsed_commas_content,
1317
			  $2, $3, ..., $n are replaced.
1318
1319
			before: only when content is not used, to go before any
1320
			  content.  For unparsed_equals, $1 is replaced with the value.
1321
			  For unparsed_commas, $1, $2, ..., $n are replaced.
1322
1323
			after: similar to before in every way, except that it is used
1324
			  when the tag is closed.
1325
1326
			disabled_content: used in place of content when the tag is
1327
			  disabled.  For closed, default is '', otherwise it is '$1' if
1328
			  block_level is false, '<div>$1</div>' elsewise.
1329
1330
			disabled_before: used in place of before when disabled.  Defaults
1331
			  to '<div>' if block_level, '' if not.
1332
1333
			disabled_after: used in place of after when disabled.  Defaults
1334
			  to '</div>' if block_level, '' if not.
1335
1336
			block_level: set to true the tag is a "block level" tag, similar
1337
			  to HTML.  Block level tags cannot be nested inside tags that are
1338
			  not block level, and will not be implicitly closed as easily.
1339
			  One break following a block level tag may also be removed.
1340
1341
			trim: if set, and 'inside' whitespace after the begin tag will be
1342
			  removed.  If set to 'outside', whitespace after the end tag will
1343
			  meet the same fate.
1344
1345
			validate: except when type is missing or 'closed', a callback to
1346
			  validate the data as $data.  Depending on the tag's type, $data
1347
			  may be a string or an array of strings (corresponding to the
1348
			  replacement.)
1349
1350
			quoted: when type is 'unparsed_equals' or 'parsed_equals' only,
1351
			  may be not set, 'optional', or 'required' corresponding to if
1352
			  the content may be quoted.  This allows the parser to read
1353
			  [tag="abc]def[esdf]"] properly.
1354
1355
			require_parents: an array of tag names, or not set.  If set, the
1356
			  enclosing tag *must* be one of the listed tags, or parsing won't
1357
			  occur.
1358
1359
			require_children: similar to require_parents, if set children
1360
			  won't be parsed if they are not in the list.
1361
1362
			disallow_children: similar to, but very different from,
1363
			  require_children, if it is set the listed tags will not be
1364
			  parsed inside the tag.
1365
1366
			parsed_tags_allowed: an array restricting what BBC can be in the
1367
			  parsed_equals parameter, if desired.
1368
		*/
1369
1370
		$codes = array(
1371
			array(
1372
				'tag' => 'abbr',
1373
				'type' => 'unparsed_equals',
1374
				'before' => '<abbr title="$1">',
1375
				'after' => '</abbr>',
1376
				'quoted' => 'optional',
1377
				'disabled_after' => ' ($1)',
1378
			),
1379
			// Legacy (and just an alias for [abbr] even when enabled)
1380
			array(
1381
				'tag' => 'acronym',
1382
				'type' => 'unparsed_equals',
1383
				'before' => '<abbr title="$1">',
1384
				'after' => '</abbr>',
1385
				'quoted' => 'optional',
1386
				'disabled_after' => ' ($1)',
1387
			),
1388
			array(
1389
				'tag' => 'anchor',
1390
				'type' => 'unparsed_equals',
1391
				'test' => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]',
1392
				'before' => '<span id="post_$1">',
1393
				'after' => '</span>',
1394
			),
1395
			array(
1396
				'tag' => 'attach',
1397
				'type' => 'unparsed_content',
1398
				'parameters' => array(
1399
					'id' => array('match' => '(\d+)'),
1400
					'alt' => array('optional' => true),
1401
					'width' => array('optional' => true, 'match' => '(\d+)'),
1402
					'height' => array('optional' => true, 'match' => '(\d+)'),
1403
					'display' => array('optional' => true, 'match' => '(link|embed)'),
1404
				),
1405
				'content' => '$1',
1406
				'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...
1407
				{
1408
					$returnContext = '';
1409
1410
					// BBC or the entire attachments feature is disabled
1411
					if (empty($modSettings['attachmentEnable']) || !empty($disabled['attach']))
1412
						return $data;
1413
1414
					// Save the attach ID.
1415
					$attachID = $params['{id}'];
1416
1417
					// Kinda need this.
1418
					require_once($sourcedir . '/Subs-Attachments.php');
1419
1420
					$currentAttachment = parseAttachBBC($attachID);
1421
1422
					// parseAttachBBC will return a string ($txt key) rather than dying with a fatal_error. Up to you to decide what to do.
1423
					if (is_string($currentAttachment))
1424
						return $data = !empty($txt[$currentAttachment]) ? $txt[$currentAttachment] : $currentAttachment;
1425
1426
					// We need a display mode.
1427
					if (empty($params['{display}']))
1428
					{
1429
						// Images, video, and audio are embedded by default.
1430
						if (!empty($currentAttachment['is_image']) || strpos($currentAttachment['mime_type'], 'video/') === 0 || strpos($currentAttachment['mime_type'], 'audio/') === 0)
1431
							$params['{display}'] = 'embed';
1432
						// Anything else shows a link by default.
1433
						else
1434
							$params['{display}'] = 'link';
1435
					}
1436
1437
					// Embedded file.
1438
					if ($params['{display}'] == 'embed')
1439
					{
1440
						$alt = ' alt="' . (!empty($params['{alt}']) ? $params['{alt}'] : $currentAttachment['name']) . '"';
1441
						$title = !empty($data) ? ' title="' . $smcFunc['htmlspecialchars']($data) . '"' : '';
1442
1443
						// Image.
1444
						if (!empty($currentAttachment['is_image']))
1445
						{
1446
							if (empty($params['{width}']) && empty($params['{height}']))
1447
								$returnContext .= '<img src="' . $currentAttachment['href'] . '"' . $alt . $title . ' class="bbc_img">';
1448
							else
1449
							{
1450
								$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"': '';
1451
								$height = !empty($params['{height}']) ? 'height="' . $params['{height}'] . '"' : '';
1452
								$returnContext .= '<img src="' . $currentAttachment['href'] . ';image"' . $alt . $title . $width . $height . ' class="bbc_img resized"/>';
1453
							}
1454
						}
1455
						// Video.
1456
						elseif (strpos($currentAttachment['mime_type'], 'video/') === 0)
1457
						{
1458
							$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"' : '';
1459
							$height = !empty($params['{height}']) ? ' height="' . $params['{height}'] . '"' : '';
1460
1461
							$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>' : '');
1462
						}
1463
						// Audio.
1464
						elseif (strpos($currentAttachment['mime_type'], 'audio/') === 0)
1465
						{
1466
							$width = 'max-width:100%; width: ' . (!empty($params['{width}']) ? $params['{width}'] : '400') . 'px;';
1467
							$height = !empty($params['{height}']) ? 'height: ' . $params['{height}'] . 'px;' : '';
1468
1469
							$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>';
1470
						}
1471
						// Anything else.
1472
						else
1473
						{
1474
							$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"' : '';
1475
							$height = !empty($params['{height}']) ? ' height="' . $params['{height}'] . '"' : '';
1476
1477
							$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>';
1478
						}
1479
					}
1480
1481
					// No image. Show a link.
1482
					else
1483
						$returnContext .= '<a href="' . $currentAttachment['href'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . '</a>';
1484
1485
					// Use this hook to adjust the HTML output of the attach BBCode.
1486
					// If you want to work with the attachment data itself, use one of these:
1487
					// - integrate_pre_parseAttachBBC
1488
					// - integrate_post_parseAttachBBC
1489
					call_integration_hook('integrate_attach_bbc_validate', array(&$returnContext, $currentAttachment, $tag, $data, $disabled, $params));
1490
1491
					// Gotta append what we just did.
1492
					$data = $returnContext;
1493
				},
1494
			),
1495
			array(
1496
				'tag' => 'b',
1497
				'before' => '<b>',
1498
				'after' => '</b>',
1499
			),
1500
			// Legacy (equivalent to [ltr] or [rtl])
1501
			array(
1502
				'tag' => 'bdo',
1503
				'type' => 'unparsed_equals',
1504
				'before' => '<bdo dir="$1">',
1505
				'after' => '</bdo>',
1506
				'test' => '(rtl|ltr)\]',
1507
				'block_level' => true,
1508
			),
1509
			// Legacy (alias of [color=black])
1510
			array(
1511
				'tag' => 'black',
1512
				'before' => '<span style="color: black;" class="bbc_color">',
1513
				'after' => '</span>',
1514
			),
1515
			// Legacy (alias of [color=blue])
1516
			array(
1517
				'tag' => 'blue',
1518
				'before' => '<span style="color: blue;" class="bbc_color">',
1519
				'after' => '</span>',
1520
			),
1521
			array(
1522
				'tag' => 'br',
1523
				'type' => 'closed',
1524
				'content' => '<br>',
1525
			),
1526
			array(
1527
				'tag' => 'center',
1528
				'before' => '<div class="centertext">',
1529
				'after' => '</div>',
1530
				'block_level' => true,
1531
			),
1532
			array(
1533
				'tag' => 'code',
1534
				'type' => 'unparsed_content',
1535
				'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>',
1536
				// @todo Maybe this can be simplified?
1537
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1538
				{
1539
					if (!isset($disabled['code']))
1540
					{
1541
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
1542
1543
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
1544
						{
1545
							// Do PHP code coloring?
1546
							if ($php_parts[$php_i] != '&lt;?php')
1547
								continue;
1548
1549
							$php_string = '';
1550
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1551
							{
1552
								$php_string .= $php_parts[$php_i];
1553
								$php_parts[$php_i++] = '';
1554
							}
1555
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1556
						}
1557
1558
						// Fix the PHP code stuff...
1559
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1560
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1561
1562
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1563
						if (!empty($context['browser']['is_opera']))
1564
							$data .= '&nbsp;';
1565
					}
1566
				},
1567
				'block_level' => true,
1568
			),
1569
			array(
1570
				'tag' => 'code',
1571
				'type' => 'unparsed_equals_content',
1572
				'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>',
1573
				// @todo Maybe this can be simplified?
1574
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1575
				{
1576
					if (!isset($disabled['code']))
1577
					{
1578
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1579
1580
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
1581
						{
1582
							// Do PHP code coloring?
1583
							if ($php_parts[$php_i] != '&lt;?php')
1584
								continue;
1585
1586
							$php_string = '';
1587
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1588
							{
1589
								$php_string .= $php_parts[$php_i];
1590
								$php_parts[$php_i++] = '';
1591
							}
1592
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1593
						}
1594
1595
						// Fix the PHP code stuff...
1596
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1597
						$data[0] = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data[0]);
1598
1599
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1600
						if (!empty($context['browser']['is_opera']))
1601
							$data[0] .= '&nbsp;';
1602
					}
1603
				},
1604
				'block_level' => true,
1605
			),
1606
			array(
1607
				'tag' => 'color',
1608
				'type' => 'unparsed_equals',
1609
				'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]?)\))\]',
1610
				'before' => '<span style="color: $1;" class="bbc_color">',
1611
				'after' => '</span>',
1612
			),
1613
			array(
1614
				'tag' => 'email',
1615
				'type' => 'unparsed_content',
1616
				'content' => '<a href="mailto:$1" class="bbc_email">$1</a>',
1617
				// @todo Should this respect guest_hideContacts?
1618
				'validate' => function(&$tag, &$data, $disabled)
1619
				{
1620
					$data = strtr($data, array('<br>' => ''));
1621
				},
1622
			),
1623
			array(
1624
				'tag' => 'email',
1625
				'type' => 'unparsed_equals',
1626
				'before' => '<a href="mailto:$1" class="bbc_email">',
1627
				'after' => '</a>',
1628
				// @todo Should this respect guest_hideContacts?
1629
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1630
				'disabled_after' => ' ($1)',
1631
			),
1632
			// Legacy (and just a link even when not disabled)
1633
			array(
1634
				'tag' => 'flash',
1635
				'type' => 'unparsed_commas_content',
1636
				'test' => '\d+,\d+\]',
1637
				'content' => '<a href="$1" target="_blank" rel="noopener">$1</a>',
1638
				'validate' => function (&$tag, &$data, $disabled)
1639
				{
1640
					$scheme = parse_url($data[0], PHP_URL_SCHEME);
1641
					if (empty($scheme))
1642
						$data[0] = '//' . ltrim($data[0], ':/');
1643
				},
1644
			),
1645
			array(
1646
				'tag' => 'float',
1647
				'type' => 'unparsed_equals',
1648
				'test' => '(left|right)(\s+max=\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)?\]',
1649
				'before' => '<div $1>',
1650
				'after' => '</div>',
1651
				'validate' => function(&$tag, &$data, $disabled)
1652
				{
1653
					$class = 'class="bbc_float float' . (strpos($data, 'left') === 0 ? 'left' : 'right') . '"';
1654
1655
					if (preg_match('~\bmax=(\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)~', $data, $matches))
1656
						$css = ' style="max-width:' . $matches[1] . (is_numeric($matches[1]) ? 'px' : '') . '"';
1657
					else
1658
						$css = '';
1659
1660
					$data = $class . $css;
1661
				},
1662
				'trim' => 'outside',
1663
				'block_level' => true,
1664
			),
1665
			// Legacy (alias of [url] with an FTP URL)
1666
			array(
1667
				'tag' => 'ftp',
1668
				'type' => 'unparsed_content',
1669
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
1670
				'validate' => function(&$tag, &$data, $disabled)
1671
				{
1672
					$data = strtr($data, array('<br>' => ''));
1673
					$scheme = parse_url($data, PHP_URL_SCHEME);
1674
					if (empty($scheme))
1675
						$data = 'ftp://' . ltrim($data, ':/');
1676
				},
1677
			),
1678
			// Legacy (alias of [url] with an FTP URL)
1679
			array(
1680
				'tag' => 'ftp',
1681
				'type' => 'unparsed_equals',
1682
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
1683
				'after' => '</a>',
1684
				'validate' => function(&$tag, &$data, $disabled)
1685
				{
1686
					$scheme = parse_url($data, PHP_URL_SCHEME);
1687
					if (empty($scheme))
1688
						$data = 'ftp://' . ltrim($data, ':/');
1689
				},
1690
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1691
				'disabled_after' => ' ($1)',
1692
			),
1693
			array(
1694
				'tag' => 'font',
1695
				'type' => 'unparsed_equals',
1696
				'test' => '[A-Za-z0-9_,\-\s]+?\]',
1697
				'before' => '<span style="font-family: $1;" class="bbc_font">',
1698
				'after' => '</span>',
1699
			),
1700
			// Legacy (one of those things that should not be done)
1701
			array(
1702
				'tag' => 'glow',
1703
				'type' => 'unparsed_commas',
1704
				'test' => '[#0-9a-zA-Z\-]{3,12},([012]\d{1,2}|\d{1,2})(,[^]]+)?\]',
1705
				'before' => '<span style="text-shadow: $1 1px 1px 1px">',
1706
				'after' => '</span>',
1707
			),
1708
			// Legacy (alias of [color=green])
1709
			array(
1710
				'tag' => 'green',
1711
				'before' => '<span style="color: green;" class="bbc_color">',
1712
				'after' => '</span>',
1713
			),
1714
			array(
1715
				'tag' => 'html',
1716
				'type' => 'unparsed_content',
1717
				'content' => '<div>$1</div>',
1718
				'block_level' => true,
1719
				'disabled_content' => '$1',
1720
			),
1721
			array(
1722
				'tag' => 'hr',
1723
				'type' => 'closed',
1724
				'content' => '<hr>',
1725
				'block_level' => true,
1726
			),
1727
			array(
1728
				'tag' => 'i',
1729
				'before' => '<i>',
1730
				'after' => '</i>',
1731
			),
1732
			array(
1733
				'tag' => 'img',
1734
				'type' => 'unparsed_content',
1735
				'parameters' => array(
1736
					'alt' => array('optional' => true),
1737
					'title' => array('optional' => true),
1738
					'width' => array('optional' => true, 'value' => ' width="$1"', 'match' => '(\d+)'),
1739
					'height' => array('optional' => true, 'value' => ' height="$1"', 'match' => '(\d+)'),
1740
				),
1741
				'content' => '$1',
1742
				'validate' => function(&$tag, &$data, $disabled, $params)
1743
				{
1744
					$url = strtr($data, array('<br>' => ''));
1745
1746
					if (parse_url($url, PHP_URL_SCHEME) === null)
1747
						$url = '//' . ltrim($url, ':/');
1748
					else
1749
						$url = get_proxied_url($url);
1750
1751
					$alt = !empty($params['{alt}']) ? ' alt="' . $params['{alt}']. '"' : ' alt=""';
1752
					$title = !empty($params['{title}']) ? ' title="' . $params['{title}']. '"' : '';
1753
1754
					$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">';
1755
				},
1756
				'disabled_content' => '($1)',
1757
			),
1758
			array(
1759
				'tag' => 'iurl',
1760
				'type' => 'unparsed_content',
1761
				'content' => '<a href="$1" class="bbc_link">$1</a>',
1762
				'validate' => function(&$tag, &$data, $disabled)
1763
				{
1764
					$data = strtr($data, array('<br>' => ''));
1765
					$scheme = parse_url($data, PHP_URL_SCHEME);
1766
					if (empty($scheme))
1767
						$data = '//' . ltrim($data, ':/');
1768
				},
1769
			),
1770
			array(
1771
				'tag' => 'iurl',
1772
				'type' => 'unparsed_equals',
1773
				'quoted' => 'optional',
1774
				'before' => '<a href="$1" class="bbc_link">',
1775
				'after' => '</a>',
1776
				'validate' => function(&$tag, &$data, $disabled)
1777
				{
1778
					if (substr($data, 0, 1) == '#')
1779
						$data = '#post_' . substr($data, 1);
1780
					else
1781
					{
1782
						$scheme = parse_url($data, PHP_URL_SCHEME);
1783
						if (empty($scheme))
1784
							$data = '//' . ltrim($data, ':/');
1785
					}
1786
				},
1787
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1788
				'disabled_after' => ' ($1)',
1789
			),
1790
			array(
1791
				'tag' => 'justify',
1792
				'before' => '<div class="justifytext">',
1793
				'after' => '</div>',
1794
				'block_level' => true,
1795
			),
1796
			array(
1797
				'tag' => 'left',
1798
				'before' => '<div class="lefttext">',
1799
				'after' => '</div>',
1800
				'block_level' => true,
1801
			),
1802
			array(
1803
				'tag' => 'li',
1804
				'before' => '<li>',
1805
				'after' => '</li>',
1806
				'trim' => 'outside',
1807
				'require_parents' => array('list'),
1808
				'block_level' => true,
1809
				'disabled_before' => '',
1810
				'disabled_after' => '<br>',
1811
			),
1812
			array(
1813
				'tag' => 'list',
1814
				'before' => '<ul class="bbc_list">',
1815
				'after' => '</ul>',
1816
				'trim' => 'inside',
1817
				'require_children' => array('li', 'list'),
1818
				'block_level' => true,
1819
			),
1820
			array(
1821
				'tag' => 'list',
1822
				'parameters' => array(
1823
					'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)'),
1824
				),
1825
				'before' => '<ul class="bbc_list" style="list-style-type: {type};">',
1826
				'after' => '</ul>',
1827
				'trim' => 'inside',
1828
				'require_children' => array('li'),
1829
				'block_level' => true,
1830
			),
1831
			array(
1832
				'tag' => 'ltr',
1833
				'before' => '<bdo dir="ltr">',
1834
				'after' => '</bdo>',
1835
				'block_level' => true,
1836
			),
1837
			array(
1838
				'tag' => 'me',
1839
				'type' => 'unparsed_equals',
1840
				'before' => '<div class="meaction">* $1 ',
1841
				'after' => '</div>',
1842
				'quoted' => 'optional',
1843
				'block_level' => true,
1844
				'disabled_before' => '/me ',
1845
				'disabled_after' => '<br>',
1846
			),
1847
			array(
1848
				'tag' => 'member',
1849
				'type' => 'unparsed_equals',
1850
				'before' => '<a href="' . $scripturl . '?action=profile;u=$1" class="mention" data-mention="$1">@',
1851
				'after' => '</a>',
1852
			),
1853
			// Legacy (horrible memories of the 1990s)
1854
			array(
1855
				'tag' => 'move',
1856
				'before' => '<marquee>',
1857
				'after' => '</marquee>',
1858
				'block_level' => true,
1859
				'disallow_children' => array('move'),
1860
			),
1861
			array(
1862
				'tag' => 'nobbc',
1863
				'type' => 'unparsed_content',
1864
				'content' => '$1',
1865
			),
1866
			array(
1867
				'tag' => 'php',
1868
				'type' => 'unparsed_content',
1869
				'content' => '<span class="phpcode">$1</span>',
1870
				'validate' => isset($disabled['php']) ? null : function(&$tag, &$data, $disabled)
1871
				{
1872
					if (!isset($disabled['php']))
1873
					{
1874
						$add_begin = substr(trim($data), 0, 5) != '&lt;?';
1875
						$data = highlight_php_code($add_begin ? '&lt;?php ' . $data . '?&gt;' : $data);
1876
						if ($add_begin)
1877
							$data = preg_replace(array('~^(.+?)&lt;\?.{0,40}?php(?:&nbsp;|\s)~', '~\?&gt;((?:</(font|span)>)*)$~'), '$1', $data, 2);
1878
					}
1879
				},
1880
				'block_level' => false,
1881
				'disabled_content' => '$1',
1882
			),
1883
			array(
1884
				'tag' => 'pre',
1885
				'before' => '<pre>',
1886
				'after' => '</pre>',
1887
			),
1888
			array(
1889
				'tag' => 'quote',
1890
				'before' => '<blockquote><cite>' . $txt['quote'] . '</cite>',
1891
				'after' => '</blockquote>',
1892
				'trim' => 'both',
1893
				'block_level' => true,
1894
			),
1895
			array(
1896
				'tag' => 'quote',
1897
				'parameters' => array(
1898
					'author' => array('match' => '(.{1,192}?)', 'quoted' => true),
1899
				),
1900
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1901
				'after' => '</blockquote>',
1902
				'trim' => 'both',
1903
				'block_level' => true,
1904
			),
1905
			array(
1906
				'tag' => 'quote',
1907
				'type' => 'parsed_equals',
1908
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': $1</cite>',
1909
				'after' => '</blockquote>',
1910
				'trim' => 'both',
1911
				'quoted' => 'optional',
1912
				// Don't allow everything to be embedded with the author name.
1913
				'parsed_tags_allowed' => array('url', 'iurl', 'ftp'),
1914
				'block_level' => true,
1915
			),
1916
			array(
1917
				'tag' => 'quote',
1918
				'parameters' => array(
1919
					'author' => array('match' => '([^<>]{1,192}?)'),
1920
					'link' => array('match' => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|msg=\d+?|action=profile;u=\d+)'),
1921
					'date' => array('match' => '(\d+)', 'validate' => 'timeformat'),
1922
				),
1923
				'before' => '<blockquote><cite><a href="' . $scripturl . '?{link}">' . $txt['quote_from'] . ': {author} ' . $txt['search_on'] . ' {date}</a></cite>',
1924
				'after' => '</blockquote>',
1925
				'trim' => 'both',
1926
				'block_level' => true,
1927
			),
1928
			array(
1929
				'tag' => 'quote',
1930
				'parameters' => array(
1931
					'author' => array('match' => '(.{1,192}?)'),
1932
				),
1933
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1934
				'after' => '</blockquote>',
1935
				'trim' => 'both',
1936
				'block_level' => true,
1937
			),
1938
			// Legacy (alias of [color=red])
1939
			array(
1940
				'tag' => 'red',
1941
				'before' => '<span style="color: red;" class="bbc_color">',
1942
				'after' => '</span>',
1943
			),
1944
			array(
1945
				'tag' => 'right',
1946
				'before' => '<div class="righttext">',
1947
				'after' => '</div>',
1948
				'block_level' => true,
1949
			),
1950
			array(
1951
				'tag' => 'rtl',
1952
				'before' => '<bdo dir="rtl">',
1953
				'after' => '</bdo>',
1954
				'block_level' => true,
1955
			),
1956
			array(
1957
				'tag' => 's',
1958
				'before' => '<s>',
1959
				'after' => '</s>',
1960
			),
1961
			// Legacy (never a good idea)
1962
			array(
1963
				'tag' => 'shadow',
1964
				'type' => 'unparsed_commas',
1965
				'test' => '[#0-9a-zA-Z\-]{3,12},(left|right|top|bottom|[0123]\d{0,2})\]',
1966
				'before' => '<span style="text-shadow: $1 $2">',
1967
				'after' => '</span>',
1968
				'validate' => function(&$tag, &$data, $disabled)
1969
				{
1970
1971
					if ($data[1] == 'top' || (is_numeric($data[1]) && $data[1] < 50))
1972
						$data[1] = '0 -2px 1px';
1973
1974
					elseif ($data[1] == 'right' || (is_numeric($data[1]) && $data[1] < 100))
1975
						$data[1] = '2px 0 1px';
1976
1977
					elseif ($data[1] == 'bottom' || (is_numeric($data[1]) && $data[1] < 190))
1978
						$data[1] = '0 2px 1px';
1979
1980
					elseif ($data[1] == 'left' || (is_numeric($data[1]) && $data[1] < 280))
1981
						$data[1] = '-2px 0 1px';
1982
1983
					else
1984
						$data[1] = '1px 1px 1px';
1985
				},
1986
			),
1987
			array(
1988
				'tag' => 'size',
1989
				'type' => 'unparsed_equals',
1990
				'test' => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)\]',
1991
				'before' => '<span style="font-size: $1;" class="bbc_size">',
1992
				'after' => '</span>',
1993
			),
1994
			array(
1995
				'tag' => 'size',
1996
				'type' => 'unparsed_equals',
1997
				'test' => '[1-7]\]',
1998
				'before' => '<span style="font-size: $1;" class="bbc_size">',
1999
				'after' => '</span>',
2000
				'validate' => function(&$tag, &$data, $disabled)
2001
				{
2002
					$sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95);
2003
					$data = $sizes[$data] . 'em';
2004
				},
2005
			),
2006
			array(
2007
				'tag' => 'sub',
2008
				'before' => '<sub>',
2009
				'after' => '</sub>',
2010
			),
2011
			array(
2012
				'tag' => 'sup',
2013
				'before' => '<sup>',
2014
				'after' => '</sup>',
2015
			),
2016
			array(
2017
				'tag' => 'table',
2018
				'before' => '<table class="bbc_table">',
2019
				'after' => '</table>',
2020
				'trim' => 'inside',
2021
				'require_children' => array('tr'),
2022
				'block_level' => true,
2023
			),
2024
			array(
2025
				'tag' => 'td',
2026
				'before' => '<td>',
2027
				'after' => '</td>',
2028
				'require_parents' => array('tr'),
2029
				'trim' => 'outside',
2030
				'block_level' => true,
2031
				'disabled_before' => '',
2032
				'disabled_after' => '',
2033
			),
2034
			array(
2035
				'tag' => 'time',
2036
				'type' => 'unparsed_content',
2037
				'content' => '$1',
2038
				'validate' => function(&$tag, &$data, $disabled)
2039
				{
2040
					if (is_numeric($data))
2041
						$data = timeformat($data);
2042
2043
					$tag['content'] = '<span class="bbc_time">$1</span>';
2044
				},
2045
			),
2046
			array(
2047
				'tag' => 'tr',
2048
				'before' => '<tr>',
2049
				'after' => '</tr>',
2050
				'require_parents' => array('table'),
2051
				'require_children' => array('td'),
2052
				'trim' => 'both',
2053
				'block_level' => true,
2054
				'disabled_before' => '',
2055
				'disabled_after' => '',
2056
			),
2057
			// Legacy (the <tt> element is dead)
2058
			array(
2059
				'tag' => 'tt',
2060
				'before' => '<span class="monospace">',
2061
				'after' => '</span>',
2062
			),
2063
			array(
2064
				'tag' => 'u',
2065
				'before' => '<u>',
2066
				'after' => '</u>',
2067
			),
2068
			array(
2069
				'tag' => 'url',
2070
				'type' => 'unparsed_content',
2071
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
2072
				'validate' => function(&$tag, &$data, $disabled)
2073
				{
2074
					$data = strtr($data, array('<br>' => ''));
2075
					$scheme = parse_url($data, PHP_URL_SCHEME);
2076
					if (empty($scheme))
2077
						$data = '//' . ltrim($data, ':/');
2078
				},
2079
			),
2080
			array(
2081
				'tag' => 'url',
2082
				'type' => 'unparsed_equals',
2083
				'quoted' => 'optional',
2084
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
2085
				'after' => '</a>',
2086
				'validate' => function(&$tag, &$data, $disabled)
2087
				{
2088
					$scheme = parse_url($data, PHP_URL_SCHEME);
2089
					if (empty($scheme))
2090
						$data = '//' . ltrim($data, ':/');
2091
				},
2092
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
2093
				'disabled_after' => ' ($1)',
2094
			),
2095
			// Legacy (alias of [color=white])
2096
			array(
2097
				'tag' => 'white',
2098
				'before' => '<span style="color: white;" class="bbc_color">',
2099
				'after' => '</span>',
2100
			),
2101
			array(
2102
				'tag' => 'youtube',
2103
				'type' => 'unparsed_content',
2104
				'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>',
2105
				'disabled_content' => '<a href="https://www.youtube.com/watch?v=$1" target="_blank" rel="noopener">https://www.youtube.com/watch?v=$1</a>',
2106
				'block_level' => true,
2107
			),
2108
		);
2109
2110
		// Inside these tags autolink is not recommendable.
2111
		$no_autolink_tags = array(
2112
			'url',
2113
			'iurl',
2114
			'email',
2115
			'img',
2116
			'html',
2117
		);
2118
2119
		// Let mods add new BBC without hassle.
2120
		call_integration_hook('integrate_bbc_codes', array(&$codes, &$no_autolink_tags));
2121
2122
		// This is mainly for the bbc manager, so it's easy to add tags above.  Custom BBC should be added above this line.
2123
		if ($message === false)
0 ignored issues
show
introduced by
The condition $message === false is always false.
Loading history...
2124
		{
2125
			usort(
2126
				$codes,
2127
				function($a, $b)
2128
				{
2129
					return strcmp($a['tag'], $b['tag']);
2130
				}
2131
			);
2132
			return $codes;
2133
		}
2134
2135
		// So the parser won't skip them.
2136
		$itemcodes = array(
2137
			'*' => 'disc',
2138
			'@' => 'disc',
2139
			'+' => 'square',
2140
			'x' => 'square',
2141
			'#' => 'square',
2142
			'o' => 'circle',
2143
			'O' => 'circle',
2144
			'0' => 'circle',
2145
		);
2146
		if (!isset($disabled['li']) && !isset($disabled['list']))
2147
		{
2148
			foreach ($itemcodes as $c => $dummy)
2149
				$bbc_codes[$c] = array();
2150
		}
2151
2152
		// Shhhh!
2153
		if (!isset($disabled['color']))
2154
		{
2155
			$codes[] = array(
2156
				'tag' => 'chrissy',
2157
				'before' => '<span style="color: #cc0099;">',
2158
				'after' => ' :-*</span>',
2159
			);
2160
			$codes[] = array(
2161
				'tag' => 'kissy',
2162
				'before' => '<span style="color: #cc0099;">',
2163
				'after' => ' :-*</span>',
2164
			);
2165
		}
2166
		$codes[] = array(
2167
			'tag' => 'cowsay',
2168
			'parameters' => array(
2169
				'e' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => 'oo', 'validate' => function ($eyes) use ($smcFunc)
2170
					{
2171
						return $smcFunc['substr']($eyes . 'oo', 0, 2);
2172
					},
2173
				),
2174
				't' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => '  ', 'validate' => function ($tongue) use ($smcFunc)
2175
					{
2176
						return $smcFunc['substr']($tongue . '  ', 0, 2);
2177
					},
2178
				),
2179
			),
2180
			'before' => '<pre data-e="{e}" data-t="{t}"><div>',
2181
			'after' => '</div></pre>',
2182
			'block_level' => true,
2183
			'validate' => function(&$tag, &$data, $disabled, $params)
2184
			{
2185
				static $moo = true;
2186
2187
				if ($moo)
2188
				{
2189
					addInlineJavaScript("\n\t" . base64_decode(
2190
						'aWYoZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImJvdmluZV9vcmFjbGU
2191
						iKT09PW51bGwpe2xldCBzdHlsZU5vZGU9ZG9jdW1lbnQuY3JlYXRlRWx
2192
						lbWVudCgic3R5bGUiKTtzdHlsZU5vZGUuaWQ9ImJvdmluZV9vcmFjbGU
2193
						iO3N0eWxlTm9kZS5pbm5lckhUTUw9J3ByZVtkYXRhLWVdW2RhdGEtdF1
2194
						7d2hpdGUtc3BhY2U6cHJlLXdyYXA7bGluZS1oZWlnaHQ6aW5pdGlhbDt
2195
						9cHJlW2RhdGEtZV1bZGF0YS10XSA+IGRpdntkaXNwbGF5OnRhYmxlO2J
2196
						vcmRlcjoxcHggc29saWQ7Ym9yZGVyLXJhZGl1czowLjVlbTtwYWRkaW5
2197
						nOjFjaDttYXgtd2lkdGg6ODBjaDttaW4td2lkdGg6MTJjaDt9cHJlW2R
2198
						hdGEtZV1bZGF0YS10XTo6YWZ0ZXJ7ZGlzcGxheTppbmxpbmUtYmxvY2s
2199
						7bWFyZ2luLWxlZnQ6OGNoO21pbi13aWR0aDoyMGNoO2RpcmVjdGlvbjp
2200
						sdHI7Y29udGVudDpcJ1xcNUMgXCdcJyBcJ1wnIF5fX15cXEEgXCdcJyB
2201
						cXDVDIFwnXCcgKFwnIGF0dHIoZGF0YS1lKSBcJylcXDVDX19fX19fX1x
2202
						cQSBcJ1wnIFwnXCcgXCdcJyAoX18pXFw1QyBcJ1wnIFwnXCcgXCdcJyB
2203
						cJ1wnIFwnXCcgXCdcJyBcJ1wnIClcXDVDL1xcNUNcXEEgXCdcJyBcJ1w
2204
						nIFwnXCcgXCdcJyBcJyBhdHRyKGRhdGEtdCkgXCcgfHwtLS0tdyB8XFx
2205
						BIFwnXCcgXCdcJyBcJ1wnIFwnXCcgXCdcJyBcJ1wnIFwnXCcgfHwgXCd
2206
						cJyBcJ1wnIFwnXCcgXCdcJyB8fFwnO30nO2RvY3VtZW50LmdldEVsZW1
2207
						lbnRzQnlUYWdOYW1lKCJoZWFkIilbMF0uYXBwZW5kQ2hpbGQoc3R5bGV
2208
						Ob2RlKTt9'
2209
					), true);
2210
2211
					$moo = false;
2212
				}
2213
			}
2214
		);
2215
2216
		foreach ($codes as $code)
2217
		{
2218
			// Make it easier to process parameters later
2219
			if (!empty($code['parameters']))
2220
				ksort($code['parameters'], SORT_STRING);
2221
2222
			// If we are not doing every tag only do ones we are interested in.
2223
			if (empty($parse_tags) || in_array($code['tag'], $parse_tags))
2224
				$bbc_codes[substr($code['tag'], 0, 1)][] = $code;
2225
		}
2226
		$codes = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $codes is dead and can be removed.
Loading history...
2227
	}
2228
2229
	// Shall we take the time to cache this?
2230
	if ($cache_id != '' && !empty($cache_enable) && (($cache_enable >= 2 && isset($message[1000])) || isset($message[2400])) && empty($parse_tags))
2231
	{
2232
		// It's likely this will change if the message is modified.
2233
		$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']);
2234
2235
		if (($temp = cache_get_data($cache_key, 240)) != null)
2236
			return $temp;
2237
2238
		$cache_t = microtime(true);
2239
	}
2240
2241
	if ($smileys === 'print')
0 ignored issues
show
introduced by
The condition $smileys === 'print' is always false.
Loading history...
2242
	{
2243
		// [glow], [shadow], and [move] can't really be printed.
2244
		$disabled['glow'] = true;
2245
		$disabled['shadow'] = true;
2246
		$disabled['move'] = true;
2247
2248
		// Colors can't well be displayed... supposed to be black and white.
2249
		$disabled['color'] = true;
2250
		$disabled['black'] = true;
2251
		$disabled['blue'] = true;
2252
		$disabled['white'] = true;
2253
		$disabled['red'] = true;
2254
		$disabled['green'] = true;
2255
		$disabled['me'] = true;
2256
2257
		// Color coding doesn't make sense.
2258
		$disabled['php'] = true;
2259
2260
		// Links are useless on paper... just show the link.
2261
		$disabled['ftp'] = true;
2262
		$disabled['url'] = true;
2263
		$disabled['iurl'] = true;
2264
		$disabled['email'] = true;
2265
		$disabled['flash'] = true;
2266
2267
		// @todo Change maybe?
2268
		if (!isset($_GET['images']))
2269
		{
2270
			$disabled['img'] = true;
2271
			$disabled['attach'] = true;
2272
		}
2273
2274
		// Maybe some custom BBC need to be disabled for printing.
2275
		call_integration_hook('integrate_bbc_print', array(&$disabled));
2276
	}
2277
2278
	$open_tags = array();
2279
	$message = strtr($message, array("\n" => '<br>'));
2280
2281
	if (!empty($parse_tags))
2282
	{
2283
		$real_alltags_regex = $alltags_regex;
2284
		$alltags_regex = '';
2285
	}
2286
	if (empty($alltags_regex))
2287
	{
2288
		$alltags = array();
2289
		foreach ($bbc_codes as $section)
2290
		{
2291
			foreach ($section as $code)
2292
				$alltags[] = $code['tag'];
2293
		}
2294
		$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

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

2294
		$alltags_regex = '(?' . '>\b' . /** @scrutinizer ignore-type */ build_regex(array_unique($alltags)) . '\b|' . build_regex(array_keys($itemcodes)) . ')';
Loading history...
2295
	}
2296
2297
	$pos = -1;
2298
	while ($pos !== false)
2299
	{
2300
		$last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos;
2301
		preg_match('~\[/?(?=' . $alltags_regex . ')~i', $message, $matches, PREG_OFFSET_CAPTURE, $pos + 1);
2302
		$pos = isset($matches[0][1]) ? $matches[0][1] : false;
2303
2304
		// Failsafe.
2305
		if ($pos === false || $last_pos > $pos)
2306
			$pos = strlen($message) + 1;
2307
2308
		// Can't have a one letter smiley, URL, or email! (Sorry.)
2309
		if ($last_pos < $pos - 1)
2310
		{
2311
			// Make sure the $last_pos is not negative.
2312
			$last_pos = max($last_pos, 0);
2313
2314
			// Pick a block of data to do some raw fixing on.
2315
			$data = substr($message, $last_pos, $pos - $last_pos);
2316
2317
			$placeholders = array();
2318
			$placeholders_counter = 0;
2319
			// Wrap in "private use" Unicode characters to ensure there will be no conflicts.
2320
			$placeholder_template = html_entity_decode('&#xE03C;') . '%1$s' . html_entity_decode('&#xE03E;');
2321
2322
			// Take care of some HTML!
2323
			if (!empty($modSettings['enablePostHTML']) && strpos($data, '&lt;') !== false)
2324
			{
2325
				$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);
2326
2327
				// <br> should be empty.
2328
				$empty_tags = array('br', 'hr');
2329
				foreach ($empty_tags as $tag)
2330
					$data = str_replace(array('&lt;' . $tag . '&gt;', '&lt;' . $tag . '/&gt;', '&lt;' . $tag . ' /&gt;'), '<' . $tag . '>', $data);
2331
2332
				// b, u, i, s, pre... basic tags.
2333
				$closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote', 'strong');
2334
				foreach ($closable_tags as $tag)
2335
				{
2336
					$diff = substr_count($data, '&lt;' . $tag . '&gt;') - substr_count($data, '&lt;/' . $tag . '&gt;');
2337
					$data = strtr($data, array('&lt;' . $tag . '&gt;' => '<' . $tag . '>', '&lt;/' . $tag . '&gt;' => '</' . $tag . '>'));
2338
2339
					if ($diff > 0)
2340
						$data = substr($data, 0, -1) . str_repeat('</' . $tag . '>', $diff) . substr($data, -1);
2341
				}
2342
2343
				// Do <img ...> - with security... action= -> action-.
2344
				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);
2345
				if (!empty($matches[0]))
2346
				{
2347
					$replaces = array();
2348
					foreach ($matches[2] as $match => $imgtag)
2349
					{
2350
						$alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^&quot;|&quot;$~', '', $matches[3][$match]);
2351
2352
						// Remove action= from the URL - no funny business, now.
2353
						// @todo Testing this preg_match seems pointless
2354
						if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) != 0)
2355
							$imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag);
2356
2357
						$placeholder = sprintf($placeholder_template, ++$placeholders_counter);
2358
						$placeholders[$placeholder] = '[img' . $alt . ']' . $imgtag . '[/img]';
2359
2360
						$replaces[$matches[0][$match]] = $placeholder;
2361
					}
2362
2363
					$data = strtr($data, $replaces);
2364
				}
2365
			}
2366
2367
			if (!empty($modSettings['autoLinkUrls']))
2368
			{
2369
				if (!function_exists('idn_to_ascii'))
2370
					require_once($sourcedir . '/Subs-Compat.php');
2371
2372
				// Are we inside tags that should be auto linked?
2373
				$no_autolink_area = false;
2374
				if (!empty($open_tags))
2375
				{
2376
					foreach ($open_tags as $open_tag)
2377
						if (in_array($open_tag['tag'], $no_autolink_tags))
2378
							$no_autolink_area = true;
2379
				}
2380
2381
				// Don't go backwards.
2382
				// @todo Don't think is the real solution....
2383
				$lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0;
2384
				if ($pos < $lastAutoPos)
2385
					$no_autolink_area = true;
2386
				$lastAutoPos = $pos;
2387
2388
				if (!$no_autolink_area)
2389
				{
2390
					// An &nbsp; right after a URL can break the autolinker
2391
					if (strpos($data, '&nbsp;') !== false)
2392
					{
2393
						$placeholders[html_entity_decode('&nbsp;', null, $context['character_set'])] = '&nbsp;';
0 ignored issues
show
Bug introduced by
null of type null is incompatible with the type integer expected by parameter $flags of html_entity_decode(). ( Ignorable by Annotation )

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

2393
						$placeholders[html_entity_decode('&nbsp;', /** @scrutinizer ignore-type */ null, $context['character_set'])] = '&nbsp;';
Loading history...
2394
						$data = strtr($data, array('&nbsp;' => html_entity_decode('&nbsp;', null, $context['character_set'])));
2395
					}
2396
2397
					// Some reusable character classes
2398
					$excluded_trailing_chars = '!;:.,?';
2399
					$domain_label_chars = '0-9A-Za-z\-' . ($context['utf8'] ? implode('', array(
2400
						'\x{A0}-\x{D7FF}', '\x{F900}-\x{FDCF}', '\x{FDF0}-\x{FFEF}',
2401
						'\x{10000}-\x{1FFFD}', '\x{20000}-\x{2FFFD}', '\x{30000}-\x{3FFFD}',
2402
						'\x{40000}-\x{4FFFD}', '\x{50000}-\x{5FFFD}', '\x{60000}-\x{6FFFD}',
2403
						'\x{70000}-\x{7FFFD}', '\x{80000}-\x{8FFFD}', '\x{90000}-\x{9FFFD}',
2404
						'\x{A0000}-\x{AFFFD}', '\x{B0000}-\x{BFFFD}', '\x{C0000}-\x{CFFFD}',
2405
						'\x{D0000}-\x{DFFFD}', '\x{E1000}-\x{EFFFD}',
2406
					)) : '');
2407
2408
					// Parse any URLs
2409
					if (!isset($disabled['url']) && strpos($data, '[url') === false)
2410
					{
2411
						// URI schemes that require some sort of special handling.
2412
						$schemes = array(
2413
							// Schemes whose URI definitions require a domain name in the
2414
							// authority (or whatever the next part of the URI is).
2415
							'need_domain' => array(
2416
								'aaa', 'aaas', 'acap', 'acct', 'afp', 'cap', 'cid', 'coap',
2417
								'coap+tcp', 'coap+ws', 'coaps', 'coaps+tcp', 'coaps+ws', 'crid',
2418
								'cvs', 'dict', 'dns', 'feed', 'fish', 'ftp', 'git', 'go',
2419
								'gopher', 'h323', 'http', 'https', 'iax', 'icap', 'im', 'imap',
2420
								'ipp', 'ipps', 'irc', 'irc6', 'ircs', 'ldap', 'ldaps', 'mailto',
2421
								'mid', 'mupdate', 'nfs', 'nntp', 'pop', 'pres', 'reload',
2422
								'rsync', 'rtsp', 'sftp', 'sieve', 'sip', 'sips', 'smb', 'snmp',
2423
								'soap.beep', 'soap.beeps', 'ssh', 'svn', 'stun', 'stuns',
2424
								'telnet', 'tftp', 'tip', 'tn3270', 'turn', 'turns', 'tv', 'udp',
2425
								'vemmi', 'vnc', 'webcal', 'ws', 'wss', 'xmlrpc.beep',
2426
								'xmlrpc.beeps', 'xmpp', 'z39.50', 'z39.50r', 'z39.50s',
2427
							),
2428
							// Schemes that allow an empty authority ("://" followed by "/")
2429
							'empty_authority' => array(
2430
								'file', 'ni', 'nih',
2431
							),
2432
							// Schemes that do not use an authority but still have a reasonable
2433
							// chance of working as clickable links.
2434
							'no_authority' => array(
2435
								'about', 'callto', 'geo', 'gg', 'leaptofrogans', 'magnet',
2436
								'mailto', 'maps', 'news', 'ni', 'nih', 'service', 'skype',
2437
								'sms', 'tel', 'tv',
2438
							),
2439
							// Schemes that we should never link.
2440
							'forbidden' => array(
2441
								'javascript', 'data',
2442
							),
2443
						);
2444
2445
						// In case a mod wants to control behaviour for a special URI scheme.
2446
						call_integration_hook('integrate_autolinker_schemes', array(&$schemes));
2447
2448
						// Don't repeat this unnecessarily.
2449
						if (empty($url_regex))
2450
						{
2451
							// PCRE subroutines for efficiency.
2452
							$pcre_subroutines = array(
2453
								'tlds' => $modSettings['tld_regex'],
2454
								'pct' => '%[0-9A-Fa-f]{2}',
2455
								'domain_label_char' => '[' . $domain_label_chars . ']',
2456
								'not_domain_label_char' => '[^' . $domain_label_chars . ']',
2457
								'domain' => '(?:(?P>domain_label_char)+\.)+(?P>tlds)',
2458
								'no_domain' => '(?:(?P>domain_label_char)|[._\~!$&\'()*+,;=:@]|(?P>pct))+',
2459
								'scheme_need_domain' => build_regex($schemes['need_domain'], '~'),
2460
								'scheme_empty_authority' => build_regex($schemes['empty_authority'], '~'),
2461
								'scheme_no_authority' => build_regex($schemes['no_authority'], '~'),
2462
								'scheme_any' => '[A-Za-z][0-9A-Za-z+\-.]*',
2463
								'user_info' => '(?:(?P>domain_label_char)|[._\~!$&\'()*+,;=:]|(?P>pct))+',
2464
								'dec_octet' => '(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)',
2465
								'h16' => '[0-9A-Fa-f]{1,4}',
2466
								'ipv4' => '(?:\b(?:(?P>dec_octet)\.){3}(?P>dec_octet)\b)',
2467
								'ipv6' => '\[(?:' . implode('|', array(
2468
									'(?:(?P>h16):){7}(?P>h16)',
2469
									'(?:(?P>h16):){1,7}:',
2470
									'(?:(?P>h16):){1,6}(?::(?P>h16))',
2471
									'(?:(?P>h16):){1,5}(?::(?P>h16)){1,2}',
2472
									'(?:(?P>h16):){1,4}(?::(?P>h16)){1,3}',
2473
									'(?:(?P>h16):){1,3}(?::(?P>h16)){1,4}',
2474
									'(?:(?P>h16):){1,2}(?::(?P>h16)){1,5}',
2475
									'(?P>h16):(?::(?P>h16)){1,6}',
2476
									':(?:(?::(?P>h16)){1,7}|:)',
2477
									'fe80:(?::(?P>h16)){0,4}%[0-9A-Za-z]+',
2478
									'::(ffff(:0{1,4})?:)?(?P>ipv4)',
2479
									'(?:(?P>h16):){1,4}:(?P>ipv4)',
2480
								)) . ')\]',
2481
								'host' => '(?:' . implode('|', array(
2482
									'localhost',
2483
									'(?P>domain)',
2484
									'(?P>ipv4)',
2485
									'(?P>ipv6)',
2486
								)) . ')',
2487
								'authority' => '(?:(?P>user_info)@)?(?P>host)(?::\d+)?',
2488
							);
2489
2490
							// Brackets and quotation marks are problematic at the end of an IRI.
2491
							// E.g.: `http://foo.com/baz(qux)` vs. `(http://foo.com/baz_qux)`
2492
							// In the first case, the user probably intended the `)` as part of the
2493
							// IRI, but not in the second case. To account for this, we test for
2494
							// balanced pairs within the IRI.
2495
							$balanced_pairs = array(
2496
								// Brackets and parentheses
2497
								'(' => ')', '[' => ']', '{' => '}',
2498
								// Double quotation marks
2499
								'"' => '"',
2500
								html_entity_decode('&#x201C;', null, $context['character_set']) => html_entity_decode('&#x201D;', null, $context['character_set']),
2501
								html_entity_decode('&#x201E;', null, $context['character_set']) => html_entity_decode('&#x201D;', null, $context['character_set']),
2502
								html_entity_decode('&#x201F;', null, $context['character_set']) => html_entity_decode('&#x201D;', null, $context['character_set']),
2503
								html_entity_decode('&#x00AB;', null, $context['character_set']) => html_entity_decode('&#x00BB;', null, $context['character_set']),
2504
								// Single quotation marks
2505
								'\'' => '\'',
2506
								html_entity_decode('&#x2018;', null, $context['character_set']) => html_entity_decode('&#x2019;', null, $context['character_set']),
2507
								html_entity_decode('&#x201A;', null, $context['character_set']) => html_entity_decode('&#x2019;', null, $context['character_set']),
2508
								html_entity_decode('&#x201B;', null, $context['character_set']) => html_entity_decode('&#x2019;', null, $context['character_set']),
2509
								html_entity_decode('&#x2039;', null, $context['character_set']) => html_entity_decode('&#x203A;', null, $context['character_set']),
2510
							);
2511
							foreach ($balanced_pairs as $pair_opener => $pair_closer)
2512
								$balanced_pairs[$smcFunc['htmlspecialchars']($pair_opener)] = $smcFunc['htmlspecialchars']($pair_closer);
2513
2514
							$bracket_quote_chars = '';
2515
							$bracket_quote_entities = array();
2516
							foreach ($balanced_pairs as $pair_opener => $pair_closer)
2517
							{
2518
								if ($pair_opener == $pair_closer)
2519
									$pair_closer = '';
2520
2521
								foreach (array($pair_opener, $pair_closer) as $bracket_quote)
2522
								{
2523
									if (strpos($bracket_quote, '&') === false)
2524
										$bracket_quote_chars .= $bracket_quote;
2525
									else
2526
										$bracket_quote_entities[] = substr($bracket_quote, 1);
2527
								}
2528
							}
2529
							$bracket_quote_chars = str_replace(array('[', ']'), array('\[', '\]'), $bracket_quote_chars);
2530
2531
							$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

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

2532
							$pcre_subroutines['allowed_entities'] = '&(?!' . /** @scrutinizer ignore-type */ build_regex(array_merge($bracket_quote_entities, array('lt;', 'gt;')), '~') . ')';
Loading history...
2533
							$pcre_subroutines['excluded_lookahead'] = '(?![' . $excluded_trailing_chars . ']*(?>[\h\v]|<br>|$))';
2534
2535
							foreach (array('path', 'query', 'fragment') as $part)
2536
							{
2537
								switch ($part) {
2538
									case 'path':
2539
										$part_disallowed_chars = '\h\v<>' . $bracket_quote_chars . $excluded_trailing_chars . '/#&';
2540
										$part_excluded_trailing_chars = str_replace('?', '', $excluded_trailing_chars);
2541
										break;
2542
2543
									case 'query':
2544
										$part_disallowed_chars = '\h\v<>' . $bracket_quote_chars . $excluded_trailing_chars . '#&';
2545
										$part_excluded_trailing_chars = $excluded_trailing_chars;
2546
										break;
2547
2548
									default:
2549
										$part_disallowed_chars = '\h\v<>' . $bracket_quote_chars . $excluded_trailing_chars . '&';
2550
										$part_excluded_trailing_chars = $excluded_trailing_chars;
2551
										break;
2552
								}
2553
								$pcre_subroutines[$part . '_allowed'] = '[^' . $part_disallowed_chars . ']|(?P>allowed_entities)|[' . $part_excluded_trailing_chars . '](?P>excluded_lookahead)';
2554
2555
								$balanced_construct_regex = array();
2556
2557
								foreach ($balanced_pairs as $pair_opener => $pair_closer)
2558
									$balanced_construct_regex[] = preg_quote($pair_opener) . '(?P>' . $part . '_recursive)*+' . preg_quote($pair_closer);
2559
2560
								$pcre_subroutines[$part . '_balanced'] = '(?:' . implode('|', $balanced_construct_regex) . ')(?P>' . $part . '_allowed)*+';
2561
								$pcre_subroutines[$part . '_recursive'] = '(?' . '>(?P>' . $part . '_allowed)|(?P>' . $part . '_balanced))';
2562
2563
								$pcre_subroutines[$part . '_segment'] =
2564
									// Allowed characters besides brackets and quotation marks
2565
									'(?P>' . $part . '_allowed)*+' .
2566
									// Brackets and quotation marks that are either...
2567
									'(?:' .
2568
										// part of a balanced construct
2569
										'(?P>' . $part . '_balanced)' .
2570
										// or
2571
										'|' .
2572
										// unpaired but not at the end
2573
										'(?P>bracket_quote)(?=(?P>' . $part . '_allowed))' .
2574
									')*+';
2575
							}
2576
2577
							// Time to build this monster!
2578
							$url_regex =
2579
							// 1. IRI scheme and domain components
2580
							'(?:' .
2581
								// 1a. IRIs with a scheme, or at least an opening "//"
2582
								'(?:' .
2583
2584
									// URI scheme (or lack thereof for schemeless URLs)
2585
									'(?:' .
2586
										// URI scheme and colon
2587
										'\b' .
2588
										'(?:' .
2589
											// Either a scheme that need a domain in the authority
2590
											// (Remember for later that we need a domain)
2591
											'(?P<need_domain>(?P>scheme_need_domain)):' .
2592
											// or
2593
											'|' .
2594
											// a scheme that allows an empty authority
2595
											// (Remember for later that the authority can be empty)
2596
											'(?P<empty_authority>(?P>scheme_empty_authority)):' .
2597
											// or
2598
											'|' .
2599
											// a scheme that uses no authority
2600
											'(?P>scheme_no_authority):(?!//)' .
2601
											// or
2602
											'|' .
2603
											// another scheme, but only if it is followed by "://"
2604
											'(?P>scheme_any):(?=//)' .
2605
										')' .
2606
2607
										// or
2608
										'|' .
2609
2610
										// An empty string followed by "//" for schemeless URLs
2611
										'(?P<schemeless>(?=//))' .
2612
									')' .
2613
2614
									// IRI authority chunk (maybe)
2615
									'(?:' .
2616
										// (Keep track of whether we find a valid authority or not)
2617
										'(?P<has_authority>' .
2618
											// 2 slashes before the authority itself
2619
											'//' .
2620
											'(?:' .
2621
												// If there was no scheme...
2622
												'(?(<schemeless>)' .
2623
													// require an authority that contains a domain.
2624
													'(?P>authority)' .
2625
2626
													// Else if a domain is needed...
2627
													'|(?(<need_domain>)' .
2628
														// require an authority with a domain.
2629
														'(?P>authority)' .
2630
2631
														// Else if an empty authority is allowed...
2632
														'|(?(<empty_authority>)' .
2633
															// then require either
2634
															'(?:' .
2635
																// empty string, followed by a "/"
2636
																'(?=/)' .
2637
																// or
2638
																'|' .
2639
																// an authority with a domain.
2640
																'(?P>authority)' .
2641
															')' .
2642
2643
															// Else just a run of IRI characters.
2644
															'|(?P>no_domain)' .
2645
														')' .
2646
													')' .
2647
												')' .
2648
											')' .
2649
											// Followed by a non-domain character or end of line
2650
											'(?=(?P>not_domain_label_char)|$)' .
2651
										')' .
2652
2653
										// or, if there is a scheme but no authority
2654
										// (e.g. "mailto:" URLs)...
2655
										'|' .
2656
2657
										// A run of IRI characters
2658
										'(?P>no_domain)' .
2659
										// If scheme needs a domain, require a dot and a TLD
2660
										'(?(<need_domain>)\.(?P>tlds))' .
2661
										// Followed by a non-domain character or end of line
2662
										'(?=(?P>not_domain_label_char)|$)' .
2663
									')' .
2664
								')' .
2665
2666
								// Or, if there is neither a scheme nor an authority...
2667
								'|' .
2668
2669
								// 1b. Naked domains
2670
								// (e.g. "example.com" in "Go to example.com for an example.")
2671
								'(?P<naked_domain>' .
2672
									// Preceded by start of line or a space
2673
									'(?<=^|<br>|[\h\v])' .
2674
									// A domain name
2675
									'(?P>domain)' .
2676
									// Followed by a non-domain character or end of line
2677
									'(?=(?P>not_domain_label_char)|$)' .
2678
								')' .
2679
							')' .
2680
2681
							// 2. IRI path, query, and fragment components (if present)
2682
							'(?:' .
2683
								// If the IRI has an authority or is a naked domain and any of these
2684
								// components exist, the path must start with a single "/".
2685
								// Note: technically, it is valid to append a query or fragment
2686
								// directly to the authority chunk without a "/", but supporting
2687
								// that in the autolinker would produce a lot of false positives,
2688
								// so we don't.
2689
								'(?=' .
2690
									// If we found an authority above...
2691
									'(?(<has_authority>)' .
2692
										// require a "/"
2693
										'/' .
2694
										// Else if we found a naked domain above...
2695
										'|(?(<naked_domain>)' .
2696
											// require a "/"
2697
											'/' .
2698
										')' .
2699
									')' .
2700
								')' .
2701
2702
								// 2.a. Path component, if any.
2703
								'(?:' .
2704
									// Can have one or more segments
2705
									'(?:' .
2706
										// Not preceded by a "/", except in the special case of an
2707
										// empty authority immediately before the path.
2708
										'(?(<empty_authority>)' .
2709
											'(?:(?<=://)|(?<!/))' .
2710
											'|' .
2711
											'(?<!/)' .
2712
										')' .
2713
										// Initial "/"
2714
										'/' .
2715
										// Then a run of allowed path segement characters
2716
										'(?P>path_segment)*+' .
2717
									')*+' .
2718
								')' .
2719
2720
								// 2.b. Query component, if any.
2721
								'(?:' .
2722
									// Initial "?" that is not last character.
2723
									'\?' . '(?=(?P>bracket_quote)*(?P>query_allowed))' .
2724
									// Then a run of allowed query characters
2725
									'(?P>query_segment)*+' .
2726
								')?' .
2727
2728
								// 2.c. Fragment component, if any.
2729
								'(?:' .
2730
									// Initial "#" that is not last character.
2731
									'#' . '(?=(?P>bracket_quote)*(?P>fragment_allowed))' .
2732
									// Then a run of allowed fragment characters
2733
									'(?P>fragment_segment)*+' .
2734
								')?' .
2735
							')?+';
2736
2737
							// Finally, define the PCRE subroutines in the regex.
2738
							$url_regex .= '(?(DEFINE)';
2739
2740
							foreach ($pcre_subroutines as $name => $subroutine)
2741
								$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

2741
								$url_regex .= '(?<' . $name . '>' . /** @scrutinizer ignore-type */ $subroutine . ')';
Loading history...
2742
2743
							$url_regex .= ')';
2744
						}
2745
2746
						$tmp_data = preg_replace_callback(
2747
							'~' . $url_regex . '~i' . ($context['utf8'] ? 'u' : ''),
2748
							function($matches) use ($schemes)
2749
							{
2750
								$url = array_shift($matches);
2751
2752
								// If this isn't a clean URL, bail out
2753
								if ($url != sanitize_iri($url))
2754
									return $url;
2755
2756
								$parsedurl = parse_url($url);
2757
2758
								if (!isset($parsedurl['scheme']))
2759
									$parsedurl['scheme'] = '';
2760
2761
								if ($parsedurl['scheme'] == 'mailto')
2762
								{
2763
									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...
2764
										return $url;
2765
2766
									// Is this version of PHP capable of validating this email address?
2767
									$can_validate = defined('FILTER_FLAG_EMAIL_UNICODE') || strlen($parsedurl['path']) == strspn(strtolower($parsedurl['path']), 'abcdefghijklmnopqrstuvwxyz0123456789!#$%&\'*+-/=?^_`{|}~.@');
2768
2769
									$flags = defined('FILTER_FLAG_EMAIL_UNICODE') ? FILTER_FLAG_EMAIL_UNICODE : null;
2770
2771
									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

2771
									if (!$can_validate || filter_var($parsedurl['path'], FILTER_VALIDATE_EMAIL, /** @scrutinizer ignore-type */ $flags) !== false)
Loading history...
2772
										return '[email=' . str_replace('mailto:', '', $url) . ']' . $url . '[/email]';
2773
									else
2774
										return $url;
2775
								}
2776
2777
								// Are we linking a schemeless URL or naked domain name (e.g. "example.com")?
2778
								if (empty($parsedurl['scheme']))
2779
									$fullUrl = '//' . ltrim($url, ':/');
2780
								else
2781
									$fullUrl = $url;
2782
2783
								// Ensure the host name is in its canonical form.
2784
								$host = !empty($parsedurl['host']) ? $parsedurl['host'] : parse_url($fullUrl, PHP_URL_HOST);
2785
2786
								if (!empty($host))
2787
								{
2788
									$ascii_host = idn_to_ascii($host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
2789
2790
									if ($ascii_host !== $host)
2791
									{
2792
										$fullUrl = substr($fullUrl, 0, strpos($fullUrl, $host)) . $ascii_host . substr($fullUrl, strpos($fullUrl, $host) + strlen($host));
2793
2794
										$utf8_host = idn_to_utf8($ascii_host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
2795
2796
										if ($utf8_host !== $host)
2797
										{
2798
											$url = substr($url, 0, strpos($url, $host)) . $utf8_host . substr($url, strpos($url, $host) + strlen($host));
2799
										}
2800
									}
2801
								}
2802
2803
								// Make sure that $fullUrl really is valid
2804
								if (in_array($parsedurl['scheme'], $schemes['forbidden']) || (!in_array($parsedurl['scheme'], $schemes['no_authority']) && validate_iri((strpos($fullUrl, '//') === 0 ? 'http:' : '') . $fullUrl) === false))
2805
									return $url;
2806
2807
								return '[url=&quot;' . str_replace(array('[', ']'), array('&#91;', '&#93;'), $fullUrl) . '&quot;]' . $url . '[/url]';
2808
							},
2809
							$data
2810
						);
2811
2812
						if (!is_null($tmp_data))
2813
							$data = $tmp_data;
2814
					}
2815
2816
					// Next, emails...  Must be careful not to step on enablePostHTML logic above...
2817
					if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false && stripos($data, 'mailto:') === false)
2818
					{
2819
						// Preceded by a space or start of line
2820
						$email_regex = '(?<=^|<br>|[\h\v])' .
2821
2822
						// An email address
2823
						'[' . $domain_label_chars . '_.]{1,80}' .
2824
						'@' .
2825
						'[' . $domain_label_chars . '.]+' .
2826
						'\.' . $modSettings['tld_regex'] .
2827
2828
						// Followed by a non-domain character or end of line
2829
						'(?=[^' . $domain_label_chars . ']|$)';
2830
2831
						$tmp_data = preg_replace('~' . $email_regex . '~i' . ($context['utf8'] ? 'u' : ''), '[email]$0[/email]', $data);
2832
2833
						if (!is_null($tmp_data))
2834
							$data = $tmp_data;
2835
					}
2836
2837
					// Save a little memory.
2838
					unset($tmp_data);
2839
				}
2840
			}
2841
2842
			// Restore any placeholders
2843
			$data = strtr($data, $placeholders);
2844
2845
			$data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));
2846
2847
			// If it wasn't changed, no copying or other boring stuff has to happen!
2848
			if ($data != substr($message, $last_pos, $pos - $last_pos))
2849
			{
2850
				$message = substr($message, 0, $last_pos) . $data . substr($message, $pos);
2851
2852
				// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
2853
				$old_pos = strlen($data) + $last_pos;
2854
				$pos = strpos($message, '[', $last_pos);
2855
				$pos = $pos === false ? $old_pos : min($pos, $old_pos);
2856
			}
2857
		}
2858
2859
		// Are we there yet?  Are we there yet?
2860
		if ($pos >= strlen($message) - 1)
2861
			break;
2862
2863
		$tag_character = strtolower($message[$pos + 1]);
2864
2865
		if ($tag_character == '/' && !empty($open_tags))
2866
		{
2867
			$pos2 = strpos($message, ']', $pos + 1);
2868
			if ($pos2 == $pos + 2)
2869
				continue;
2870
2871
			$look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));
2872
2873
			// A closing tag that doesn't match any open tags? Skip it.
2874
			if (!in_array($look_for, array_map(function($code) { return $code['tag']; }, $open_tags)))
2875
				continue;
2876
2877
			$to_close = array();
2878
			$block_level = null;
2879
2880
			do
2881
			{
2882
				$tag = array_pop($open_tags);
2883
				if (!$tag)
2884
					break;
2885
2886
				if (!empty($tag['block_level']))
2887
				{
2888
					// Only find out if we need to.
2889
					if ($block_level === false)
2890
					{
2891
						array_push($open_tags, $tag);
2892
						break;
2893
					}
2894
2895
					// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
2896
					if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]]))
2897
					{
2898
						foreach ($bbc_codes[$look_for[0]] as $temp)
2899
							if ($temp['tag'] == $look_for)
2900
							{
2901
								$block_level = !empty($temp['block_level']);
2902
								break;
2903
							}
2904
					}
2905
2906
					if ($block_level !== true)
2907
					{
2908
						$block_level = false;
2909
						array_push($open_tags, $tag);
2910
						break;
2911
					}
2912
				}
2913
2914
				$to_close[] = $tag;
2915
			}
2916
			while ($tag['tag'] != $look_for);
2917
2918
			// Did we just eat through everything and not find it?
2919
			if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
2920
			{
2921
				$open_tags = $to_close;
2922
				continue;
2923
			}
2924
			elseif (!empty($to_close) && $tag['tag'] != $look_for)
2925
			{
2926
				if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]]))
2927
				{
2928
					foreach ($bbc_codes[$look_for[0]] as $temp)
2929
						if ($temp['tag'] == $look_for)
2930
						{
2931
							$block_level = !empty($temp['block_level']);
2932
							break;
2933
						}
2934
				}
2935
2936
				// We're not looking for a block level tag (or maybe even a tag that exists...)
2937
				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...
2938
				{
2939
					foreach ($to_close as $tag)
2940
						array_push($open_tags, $tag);
2941
					continue;
2942
				}
2943
			}
2944
2945
			foreach ($to_close as $tag)
2946
			{
2947
				$message = substr($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1);
2948
				$pos += strlen($tag['after']) + 2;
2949
				$pos2 = $pos - 1;
2950
2951
				// See the comment at the end of the big loop - just eating whitespace ;).
2952
				$whitespace_regex = '';
2953
				if (!empty($tag['block_level']))
2954
					$whitespace_regex .= '(&nbsp;|\s)*(<br\s*/?' . '>)?';
2955
				// Trim one line of whitespace after unnested tags, but all of it after nested ones
2956
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
2957
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2958
2959
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
2960
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2961
			}
2962
2963
			if (!empty($to_close))
2964
			{
2965
				$to_close = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $to_close is dead and can be removed.
Loading history...
2966
				$pos--;
2967
			}
2968
2969
			continue;
2970
		}
2971
2972
		// No tags for this character, so just keep going (fastest possible course.)
2973
		if (!isset($bbc_codes[$tag_character]))
2974
			continue;
2975
2976
		$inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
2977
		$tag = null;
2978
		foreach ($bbc_codes[$tag_character] as $possible)
2979
		{
2980
			$pt_strlen = strlen($possible['tag']);
2981
2982
			// Not a match?
2983
			if (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag'])
2984
				continue;
2985
2986
			$next_c = isset($message[$pos + 1 + $pt_strlen]) ? $message[$pos + 1 + $pt_strlen] : '';
2987
2988
			// A tag is the last char maybe
2989
			if ($next_c == '')
2990
				break;
2991
2992
			// A test validation?
2993
			if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0)
2994
				continue;
2995
			// Do we want parameters?
2996
			elseif (!empty($possible['parameters']))
2997
			{
2998
				// Are all the parameters optional?
2999
				$param_required = false;
3000
				foreach ($possible['parameters'] as $param)
3001
				{
3002
					if (empty($param['optional']))
3003
					{
3004
						$param_required = true;
3005
						break;
3006
					}
3007
				}
3008
3009
				if ($param_required && $next_c != ' ')
3010
					continue;
3011
			}
3012
			elseif (isset($possible['type']))
3013
			{
3014
				// Do we need an equal sign?
3015
				if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=')
3016
					continue;
3017
				// Maybe we just want a /...
3018
				if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + $pt_strlen, 2) != '/]' && substr($message, $pos + 1 + $pt_strlen, 3) != ' /]')
3019
					continue;
3020
				// An immediate ]?
3021
				if ($possible['type'] == 'unparsed_content' && $next_c != ']')
3022
					continue;
3023
			}
3024
			// No type means 'parsed_content', which demands an immediate ] without parameters!
3025
			elseif ($next_c != ']')
3026
				continue;
3027
3028
			// Check allowed tree?
3029
			if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
3030
				continue;
3031
			elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
3032
				continue;
3033
			// If this is in the list of disallowed child tags, don't parse it.
3034
			elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
3035
				continue;
3036
3037
			$pos1 = $pos + 1 + $pt_strlen + 1;
3038
3039
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
3040
			if ($possible['tag'] == 'quote')
3041
			{
3042
				// Start with standard
3043
				$quote_alt = false;
3044
				foreach ($open_tags as $open_quote)
3045
				{
3046
					// Every parent quote this quote has flips the styling
3047
					if ($open_quote['tag'] == 'quote')
3048
						$quote_alt = !$quote_alt;
0 ignored issues
show
introduced by
The condition $quote_alt is always false.
Loading history...
3049
				}
3050
				// Add a class to the quote to style alternating blockquotes
3051
				$possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">'));
3052
			}
3053
3054
			// This is long, but it makes things much easier and cleaner.
3055
			if (!empty($possible['parameters']))
3056
			{
3057
				// Build a regular expression for each parameter for the current tag.
3058
				$regex_key = $smcFunc['json_encode']($possible['parameters']);
3059
				if (!isset($params_regexes[$regex_key]))
3060
				{
3061
					$params_regexes[$regex_key] = '';
3062
3063
					foreach ($possible['parameters'] as $p => $info)
3064
						$params_regexes[$regex_key] .= '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . '\s*)' . (empty($info['optional']) ? '' : '?');
3065
				}
3066
3067
				// Extract the string that potentially holds our parameters.
3068
				$blob = preg_split('~\[/?(?:' . $alltags_regex . ')~i', substr($message, $pos));
3069
				$blobs = preg_split('~\]~i', $blob[1]);
3070
3071
				$splitters = implode('=|', array_keys($possible['parameters'])) . '=';
3072
3073
				// Progressively append more blobs until we find our parameters or run out of blobs
3074
				$blob_counter = 1;
3075
				while ($blob_counter <= count($blobs))
3076
				{
3077
					$given_param_string = implode(']', array_slice($blobs, 0, $blob_counter++));
3078
3079
					$given_params = preg_split('~\s(?=(' . $splitters . '))~i', $given_param_string);
3080
					sort($given_params, SORT_STRING);
3081
3082
					$match = preg_match('~^' . $params_regexes[$regex_key] . '$~i', implode(' ', $given_params), $matches) !== 0;
3083
3084
					if ($match)
3085
						break;
3086
				}
3087
3088
				// Didn't match our parameter list, try the next possible.
3089
				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...
3090
					continue;
3091
3092
				$params = array();
3093
				for ($i = 1, $n = count($matches); $i < $n; $i += 2)
3094
				{
3095
					$key = strtok(ltrim($matches[$i]), '=');
3096
					if ($key === false)
3097
						continue;
3098
					elseif (isset($possible['parameters'][$key]['value']))
3099
						$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
3100
					elseif (isset($possible['parameters'][$key]['validate']))
3101
						$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
3102
					else
3103
						$params['{' . $key . '}'] = $matches[$i + 1];
3104
3105
					// Just to make sure: replace any $ or { so they can't interpolate wrongly.
3106
					$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
3107
				}
3108
3109
				foreach ($possible['parameters'] as $p => $info)
3110
				{
3111
					if (!isset($params['{' . $p . '}']))
3112
					{
3113
						if (!isset($info['default']))
3114
							$params['{' . $p . '}'] = '';
3115
						elseif (isset($possible['parameters'][$p]['value']))
3116
							$params['{' . $p . '}'] = strtr($possible['parameters'][$p]['value'], array('$1' => $info['default']));
3117
						elseif (isset($possible['parameters'][$p]['validate']))
3118
							$params['{' . $p . '}'] = $possible['parameters'][$p]['validate']($info['default']);
3119
						else
3120
							$params['{' . $p . '}'] = $info['default'];
3121
					}
3122
				}
3123
3124
				$tag = $possible;
3125
3126
				// Put the parameters into the string.
3127
				if (isset($tag['before']))
3128
					$tag['before'] = strtr($tag['before'], $params);
3129
				if (isset($tag['after']))
3130
					$tag['after'] = strtr($tag['after'], $params);
3131
				if (isset($tag['content']))
3132
					$tag['content'] = strtr($tag['content'], $params);
3133
3134
				$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...
3135
			}
3136
			else
3137
			{
3138
				$tag = $possible;
3139
				$params = array();
3140
			}
3141
			break;
3142
		}
3143
3144
		// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
3145
		if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
3146
		{
3147
			if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
3148
				continue;
3149
3150
			$tag = $itemcodes[$message[$pos + 1]];
3151
3152
			// First let's set up the tree: it needs to be in a list, or after an li.
3153
			if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
3154
			{
3155
				$open_tags[] = array(
3156
					'tag' => 'list',
3157
					'after' => '</ul>',
3158
					'block_level' => true,
3159
					'require_children' => array('li'),
3160
					'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
3161
				);
3162
				$code = '<ul class="bbc_list">';
3163
			}
3164
			// We're in a list item already: another itemcode?  Close it first.
3165
			elseif ($inside['tag'] == 'li')
3166
			{
3167
				array_pop($open_tags);
3168
				$code = '</li>';
3169
			}
3170
			else
3171
				$code = '';
3172
3173
			// Now we open a new tag.
3174
			$open_tags[] = array(
3175
				'tag' => 'li',
3176
				'after' => '</li>',
3177
				'trim' => 'outside',
3178
				'block_level' => true,
3179
				'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
3180
			);
3181
3182
			// First, open the tag...
3183
			$code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
3184
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
3185
			$pos += strlen($code) - 1 + 2;
3186
3187
			// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
3188
			$pos2 = strpos($message, '<br>', $pos);
3189
			$pos3 = strpos($message, '[/', $pos);
3190
			if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
3191
			{
3192
				preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 4), $matches);
3193
				$message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
3194
3195
				$open_tags[count($open_tags) - 2]['after'] = '</ul>';
3196
			}
3197
			// Tell the [list] that it needs to close specially.
3198
			else
3199
			{
3200
				// Move the li over, because we're not sure what we'll hit.
3201
				$open_tags[count($open_tags) - 1]['after'] = '';
3202
				$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
3203
			}
3204
3205
			continue;
3206
		}
3207
3208
		// Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
3209
		if ($tag === null && $inside !== null && !empty($inside['require_children']))
3210
		{
3211
			array_pop($open_tags);
3212
3213
			$message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
3214
			$pos += strlen($inside['after']) - 1 + 2;
3215
		}
3216
3217
		// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
3218
		if ($tag === null)
3219
			continue;
3220
3221
		// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
3222
		if (isset($inside['disallow_children']))
3223
			$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
3224
3225
		// Is this tag disabled?
3226
		if (isset($disabled[$tag['tag']]))
3227
		{
3228
			if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
3229
			{
3230
				$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
3231
				$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
3232
				$tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
3233
			}
3234
			elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
3235
			{
3236
				$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
3237
				$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
3238
			}
3239
			else
3240
				$tag['content'] = $tag['disabled_content'];
3241
		}
3242
3243
		// we use this a lot
3244
		$tag_strlen = strlen($tag['tag']);
3245
3246
		// The only special case is 'html', which doesn't need to close things.
3247
		if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
3248
		{
3249
			$n = count($open_tags) - 1;
3250
			while (empty($open_tags[$n]['block_level']) && $n >= 0)
3251
				$n--;
3252
3253
			// Close all the non block level tags so this tag isn't surrounded by them.
3254
			for ($i = count($open_tags) - 1; $i > $n; $i--)
3255
			{
3256
				$message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
3257
				$ot_strlen = strlen($open_tags[$i]['after']);
3258
				$pos += $ot_strlen + 2;
3259
				$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...
3260
3261
				// Trim or eat trailing stuff... see comment at the end of the big loop.
3262
				$whitespace_regex = '';
3263
				if (!empty($tag['block_level']))
3264
					$whitespace_regex .= '(&nbsp;|\s)*(<br>)?';
3265
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
3266
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
3267
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
3268
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
3269
3270
				array_pop($open_tags);
3271
			}
3272
		}
3273
3274
		// Can't read past the end of the message
3275
		$pos1 = min(strlen($message), $pos1);
3276
3277
		// No type means 'parsed_content'.
3278
		if (!isset($tag['type']))
3279
		{
3280
			$open_tags[] = $tag;
3281
3282
			// There's no data to change, but maybe do something based on params?
3283
			$data = null;
3284
			if (isset($tag['validate']))
3285
				$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...
3286
3287
			$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
3288
			$pos += strlen($tag['before']) - 1 + 2;
3289
		}
3290
		// Don't parse the content, just skip it.
3291
		elseif ($tag['type'] == 'unparsed_content')
3292
		{
3293
			$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
3294
			if ($pos2 === false)
3295
				continue;
3296
3297
			$data = substr($message, $pos1, $pos2 - $pos1);
3298
3299
			if (!empty($tag['block_level']) && substr($data, 0, 4) == '<br>')
3300
				$data = substr($data, 4);
3301
3302
			if (isset($tag['validate']))
3303
				$tag['validate']($tag, $data, $disabled, $params);
3304
3305
			$code = strtr($tag['content'], array('$1' => $data));
3306
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
3307
3308
			$pos += strlen($code) - 1 + 2;
3309
			$last_pos = $pos + 1;
3310
		}
3311
		// Don't parse the content, just skip it.
3312
		elseif ($tag['type'] == 'unparsed_equals_content')
3313
		{
3314
			// The value may be quoted for some tags - check.
3315
			if (isset($tag['quoted']))
3316
			{
3317
				$quoted = substr($message, $pos1, 6) == '&quot;';
3318
				if ($tag['quoted'] != 'optional' && !$quoted)
3319
					continue;
3320
3321
				if ($quoted)
3322
					$pos1 += 6;
3323
			}
3324
			else
3325
				$quoted = false;
3326
3327
			$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...
3328
			if ($pos2 === false)
3329
				continue;
3330
3331
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3332
			if ($pos3 === false)
3333
				continue;
3334
3335
			$data = array(
3336
				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...
3337
				substr($message, $pos1, $pos2 - $pos1)
3338
			);
3339
3340
			if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '<br>')
3341
				$data[0] = substr($data[0], 4);
3342
3343
			// Validation for my parking, please!
3344
			if (isset($tag['validate']))
3345
				$tag['validate']($tag, $data, $disabled, $params);
3346
3347
			$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
3348
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3349
			$pos += strlen($code) - 1 + 2;
3350
		}
3351
		// A closed tag, with no content or value.
3352
		elseif ($tag['type'] == 'closed')
3353
		{
3354
			$pos2 = strpos($message, ']', $pos);
3355
3356
			// Maybe a custom BBC wants to do something special?
3357
			$data = null;
3358
			if (isset($tag['validate']))
3359
				$tag['validate']($tag, $data, $disabled, $params);
3360
3361
			$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
3362
			$pos += strlen($tag['content']) - 1 + 2;
3363
		}
3364
		// This one is sorta ugly... :/.  Unfortunately, it's needed for flash.
3365
		elseif ($tag['type'] == 'unparsed_commas_content')
3366
		{
3367
			$pos2 = strpos($message, ']', $pos1);
3368
			if ($pos2 === false)
3369
				continue;
3370
3371
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3372
			if ($pos3 === false)
3373
				continue;
3374
3375
			// We want $1 to be the content, and the rest to be csv.
3376
			$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
3377
			$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
3378
3379
			if (isset($tag['validate']))
3380
				$tag['validate']($tag, $data, $disabled, $params);
3381
3382
			$code = $tag['content'];
3383
			foreach ($data as $k => $d)
3384
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3385
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3386
			$pos += strlen($code) - 1 + 2;
3387
		}
3388
		// This has parsed content, and a csv value which is unparsed.
3389
		elseif ($tag['type'] == 'unparsed_commas')
3390
		{
3391
			$pos2 = strpos($message, ']', $pos1);
3392
			if ($pos2 === false)
3393
				continue;
3394
3395
			$data = explode(',', substr($message, $pos1, $pos2 - $pos1));
3396
3397
			if (isset($tag['validate']))
3398
				$tag['validate']($tag, $data, $disabled, $params);
3399
3400
			// Fix after, for disabled code mainly.
3401
			foreach ($data as $k => $d)
3402
				$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
3403
3404
			$open_tags[] = $tag;
3405
3406
			// Replace them out, $1, $2, $3, $4, etc.
3407
			$code = $tag['before'];
3408
			foreach ($data as $k => $d)
3409
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3410
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1);
3411
			$pos += strlen($code) - 1 + 2;
3412
		}
3413
		// A tag set to a value, parsed or not.
3414
		elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
3415
		{
3416
			// The value may be quoted for some tags - check.
3417
			if (isset($tag['quoted']))
3418
			{
3419
				$quoted = substr($message, $pos1, 6) == '&quot;';
3420
				if ($tag['quoted'] != 'optional' && !$quoted)
3421
					continue;
3422
3423
				if ($quoted)
3424
					$pos1 += 6;
3425
			}
3426
			else
3427
				$quoted = false;
3428
3429
			if ($quoted)
3430
			{
3431
				$end_of_value = strpos($message, '&quot;]', $pos1);
3432
				$nested_tag = strpos($message, '=&quot;', $pos1);
3433
				// Check so this is not just an quoted url ending with a =
3434
				if ($nested_tag && substr($message, $nested_tag, 8) == '=&quot;]')
3435
					$nested_tag = false;
3436
				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...
3437
					// Nested tag with quoted value detected, use next end tag
3438
					$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...
3439
			}
3440
3441
			$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...
3442
			if ($pos2 === false)
3443
				continue;
3444
3445
			$data = substr($message, $pos1, $pos2 - $pos1);
3446
3447
			// Validation for my parking, please!
3448
			if (isset($tag['validate']))
3449
				$tag['validate']($tag, $data, $disabled, $params);
3450
3451
			// For parsed content, we must recurse to avoid security problems.
3452
			if ($tag['type'] != 'unparsed_equals')
3453
				$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
3454
3455
			$tag['after'] = strtr($tag['after'], array('$1' => $data));
3456
3457
			$open_tags[] = $tag;
3458
3459
			$code = strtr($tag['before'], array('$1' => $data));
3460
			$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...
3461
			$pos += strlen($code) - 1 + 2;
3462
		}
3463
3464
		// If this is block level, eat any breaks after it.
3465
		if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '<br>')
3466
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 5);
3467
3468
		// Are we trimming outside this tag?
3469
		if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
3470
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
3471
	}
3472
3473
	// Close any remaining tags.
3474
	while ($tag = array_pop($open_tags))
3475
		$message .= "\n" . $tag['after'] . "\n";
3476
3477
	// Parse the smileys within the parts where it can be done safely.
3478
	if ($smileys === true)
3479
	{
3480
		$message_parts = explode("\n", $message);
3481
		for ($i = 0, $n = count($message_parts); $i < $n; $i += 2)
3482
			parsesmileys($message_parts[$i]);
3483
3484
		$message = implode('', $message_parts);
3485
	}
3486
3487
	// No smileys, just get rid of the markers.
3488
	else
3489
		$message = strtr($message, array("\n" => ''));
3490
3491
	if ($message !== '' && $message[0] === ' ')
3492
		$message = '&nbsp;' . substr($message, 1);
3493
3494
	// Cleanup whitespace.
3495
	$message = strtr($message, array('  ' => ' &nbsp;', "\r" => '', "\n" => '<br>', '<br> ' => '<br>&nbsp;', '&#13;' => "\n"));
3496
3497
	// Allow mods access to what parse_bbc created
3498
	call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
3499
3500
	// Cache the output if it took some time...
3501
	if (isset($cache_key, $cache_t) && microtime(true) - $cache_t > 0.05)
3502
		cache_put_data($cache_key, $message, 240);
3503
3504
	// If this was a force parse revert if needed.
3505
	if (!empty($parse_tags))
3506
	{
3507
		$alltags_regex = empty($real_alltags_regex) ? '' : $real_alltags_regex;
3508
		unset($real_alltags_regex);
3509
	}
3510
	elseif (!empty($bbc_codes))
3511
		$bbc_lang_locales[$txt['lang_locale']] = $bbc_codes;
3512
3513
	return $message;
3514
}
3515
3516
/**
3517
 * Parse smileys in the passed message.
3518
 *
3519
 * The smiley parsing function which makes pretty faces appear :).
3520
 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
3521
 * These are specifically not parsed in code tags [url=mailto:[email protected]]
3522
 * Caches the smileys from the database or array in memory.
3523
 * Doesn't return anything, but rather modifies message directly.
3524
 *
3525
 * @param string &$message The message to parse smileys in
3526
 */
3527
function parsesmileys(&$message)
3528
{
3529
	global $modSettings, $txt, $user_info, $context, $smcFunc;
3530
	static $smileyPregSearch = null, $smileyPregReplacements = array();
3531
3532
	// No smiley set at all?!
3533
	if ($user_info['smiley_set'] == 'none' || trim($message) == '')
3534
		return;
3535
3536
	// Maybe a mod wants to implement an alternative method (e.g. emojis instead of images)
3537
	call_integration_hook('integrate_smileys', array(&$smileyPregSearch, &$smileyPregReplacements));
3538
3539
	// If smileyPregSearch hasn't been set, do it now.
3540
	if (empty($smileyPregSearch))
3541
	{
3542
		// Cache for longer when customized smiley codes aren't enabled
3543
		$cache_time = empty($modSettings['smiley_enable']) ? 7200 : 480;
3544
3545
		// Load the smileys in reverse order by length so they don't get parsed incorrectly.
3546
		if (($temp = cache_get_data('parsing_smileys_' . $user_info['smiley_set'], $cache_time)) == null)
3547
		{
3548
			$result = $smcFunc['db_query']('', '
3549
				SELECT s.code, f.filename, s.description
3550
				FROM {db_prefix}smileys AS s
3551
					JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
3552
				WHERE f.smiley_set = {string:smiley_set}' . (empty($modSettings['smiley_enable']) ? '
3553
					AND s.code IN ({array_string:default_codes})' : '') . '
3554
				ORDER BY LENGTH(s.code) DESC',
3555
				array(
3556
					'default_codes' => array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'),
3557
					'smiley_set' => $user_info['smiley_set'],
3558
				)
3559
			);
3560
			$smileysfrom = array();
3561
			$smileysto = array();
3562
			$smileysdescs = array();
3563
			while ($row = $smcFunc['db_fetch_assoc']($result))
3564
			{
3565
				$smileysfrom[] = $row['code'];
3566
				$smileysto[] = $smcFunc['htmlspecialchars']($row['filename']);
3567
				$smileysdescs[] = !empty($txt['icon_' . strtolower($row['description'])]) ? $txt['icon_' . strtolower($row['description'])] : $row['description'];
3568
			}
3569
			$smcFunc['db_free_result']($result);
3570
3571
			cache_put_data('parsing_smileys_' . $user_info['smiley_set'], array($smileysfrom, $smileysto, $smileysdescs), $cache_time);
3572
		}
3573
		else
3574
			list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
3575
3576
		// The non-breaking-space is a complex thing...
3577
		$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
3578
3579
		// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:[email protected]] doesn't parse the :D smiley)
3580
		$smileyPregReplacements = array();
3581
		$searchParts = array();
3582
		$smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
3583
3584
		for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
3585
		{
3586
			$specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES);
3587
			$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">';
3588
3589
			$smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
3590
3591
			$searchParts[] = $smileysfrom[$i];
3592
			if ($smileysfrom[$i] != $specialChars)
3593
			{
3594
				$smileyPregReplacements[$specialChars] = $smileyCode;
3595
				$searchParts[] = $specialChars;
3596
3597
				// Some 2.0 hex htmlchars are in there as 3 digits; allow for finding leading 0 or not
3598
				$specialChars2 = preg_replace('/&#(\d{2});/', '&#0$1;', $specialChars);
3599
				if ($specialChars2 != $specialChars)
3600
				{
3601
					$smileyPregReplacements[$specialChars2] = $smileyCode;
3602
					$searchParts[] = $specialChars2;
3603
				}
3604
			}
3605
		}
3606
3607
		$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

3607
		$smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?<![a-zA-Z0-9])\(|^)(' . /** @scrutinizer ignore-type */ build_regex($searchParts, '~') . ')(?=[^[:alpha:]0-9]|$)~' . ($context['utf8'] ? 'u' : '');
Loading history...
3608
	}
3609
3610
	// If there are no smileys defined, no need to replace anything
3611
	if (empty($smileyPregReplacements))
3612
		return;
3613
3614
	// Replace away!
3615
	$message = preg_replace_callback(
3616
		$smileyPregSearch,
3617
		function($matches) use ($smileyPregReplacements)
3618
		{
3619
			return $smileyPregReplacements[$matches[1]];
3620
		},
3621
		$message
3622
	);
3623
}
3624
3625
/**
3626
 * Highlight any code.
3627
 *
3628
 * Uses PHP's highlight_string() to highlight PHP syntax
3629
 * does special handling to keep the tabs in the code available.
3630
 * used to parse PHP code from inside [code] and [php] tags.
3631
 *
3632
 * @param string $code The code
3633
 * @return string The code with highlighted HTML.
3634
 */
3635
function highlight_php_code($code)
3636
{
3637
	// Remove special characters.
3638
	$code = un_htmlspecialchars(strtr($code, array('<br />' => "\n", '<br>' => "\n", "\t" => 'SMF_TAB();', '&#91;' => '[')));
3639
3640
	$oldlevel = error_reporting(0);
3641
3642
	$buffer = str_replace(array("\n", "\r"), '', @highlight_string($code, true));
3643
3644
	error_reporting($oldlevel);
3645
3646
	// Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
3647
	$buffer = preg_replace('~SMF_TAB(?:</(?:font|span)><(?:font color|span style)="[^"]*?">)?\\(\\);~', '<pre style="display: inline;">' . "\t" . '</pre>', $buffer);
3648
3649
	return strtr($buffer, array('\'' => '&#039;', '<code>' => '', '</code>' => ''));
3650
}
3651
3652
/**
3653
 * Gets the appropriate URL to use for images (or whatever) when using SSL
3654
 *
3655
 * The returned URL may or may not be a proxied URL, depending on the situation.
3656
 * Mods can implement alternative proxies using the 'integrate_proxy' hook.
3657
 *
3658
 * @param string $url The original URL of the requested resource
3659
 * @return string The URL to use
3660
 */
3661
function get_proxied_url($url)
3662
{
3663
	global $boardurl, $image_proxy_enabled, $image_proxy_secret, $user_info;
3664
3665
	// Only use the proxy if enabled, and never for robots
3666
	if (empty($image_proxy_enabled) || !empty($user_info['possibly_robot']))
3667
		return $url;
3668
3669
	$parsedurl = parse_url($url);
3670
3671
	// Don't bother with HTTPS URLs, schemeless URLs, or obviously invalid URLs
3672
	if (empty($parsedurl['scheme']) || empty($parsedurl['host']) || empty($parsedurl['path']) || $parsedurl['scheme'] === 'https')
3673
		return $url;
3674
3675
	// We don't need to proxy our own resources
3676
	if ($parsedurl['host'] === parse_url($boardurl, PHP_URL_HOST))
3677
		return strtr($url, array('http://' => 'https://'));
3678
3679
	// By default, use SMF's own image proxy script
3680
	$proxied_url = strtr($boardurl, array('http://' => 'https://')) . '/proxy.php?request=' . urlencode($url) . '&hash=' . hash_hmac('sha1', $url, $image_proxy_secret);
3681
3682
	// Allow mods to easily implement an alternative proxy
3683
	// MOD AUTHORS: To add settings UI for your proxy, use the integrate_general_settings hook.
3684
	call_integration_hook('integrate_proxy', array($url, &$proxied_url));
3685
3686
	return $proxied_url;
3687
}
3688
3689
/**
3690
 * Make sure the browser doesn't come back and repost the form data.
3691
 * Should be used whenever anything is posted.
3692
 *
3693
 * @param string $setLocation The URL to redirect them to
3694
 * @param bool $refresh Whether to use a meta refresh instead
3695
 * @param bool $permanent Whether to send a 301 Moved Permanently instead of a 302 Moved Temporarily
3696
 */
3697
function redirectexit($setLocation = '', $refresh = false, $permanent = false)
3698
{
3699
	global $scripturl, $context, $modSettings, $db_show_debug, $db_cache;
3700
3701
	// In case we have mail to send, better do that - as obExit doesn't always quite make it...
3702
	if (!empty($context['flush_mail']))
3703
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
3704
		AddMailQueue(true);
3705
3706
	$add = preg_match('~^(ftp|http)[s]?://~', $setLocation) == 0 && substr($setLocation, 0, 6) != 'about:';
3707
3708
	if ($add)
3709
		$setLocation = $scripturl . ($setLocation != '' ? '?' . $setLocation : '');
3710
3711
	// Put the session ID in.
3712
	if (defined('SID') && SID != '')
3713
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', $scripturl . '?' . SID . ';', $setLocation);
3714
	// Keep that debug in their for template debugging!
3715
	elseif (isset($_GET['debug']))
3716
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);
3717
3718
	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'])))
3719
	{
3720
		if (defined('SID') && SID != '')
3721
			$setLocation = preg_replace_callback(
3722
				'~^' . preg_quote($scripturl, '~') . '\?(?:' . SID . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~',
3723
				function($m) use ($scripturl)
3724
				{
3725
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . SID . (isset($m[2]) ? "$m[2]" : "");
3726
				},
3727
				$setLocation
3728
			);
3729
		else
3730
			$setLocation = preg_replace_callback(
3731
				'~^' . preg_quote($scripturl, '~') . '\?((?:board|topic)=[^#"]+?)(#[^"]*?)?$~',
3732
				function($m) use ($scripturl)
3733
				{
3734
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html' . (isset($m[2]) ? "$m[2]" : "");
3735
				},
3736
				$setLocation
3737
			);
3738
	}
3739
3740
	// Maybe integrations want to change where we are heading?
3741
	call_integration_hook('integrate_redirect', array(&$setLocation, &$refresh, &$permanent));
3742
3743
	// Set the header.
3744
	header('location: ' . str_replace(' ', '%20', $setLocation), true, $permanent ? 301 : 302);
3745
3746
	// Debugging.
3747
	if (isset($db_show_debug) && $db_show_debug === true)
3748
		$_SESSION['debug_redirect'] = $db_cache;
3749
3750
	obExit(false);
3751
}
3752
3753
/**
3754
 * Ends execution.  Takes care of template loading and remembering the previous URL.
3755
 *
3756
 * @param bool $header Whether to do the header
3757
 * @param bool $do_footer Whether to do the footer
3758
 * @param bool $from_index Whether we're coming from the board index
3759
 * @param bool $from_fatal_error Whether we're coming from a fatal error
3760
 */
3761
function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false)
3762
{
3763
	global $context, $settings, $modSettings, $txt, $smcFunc, $should_log;
3764
	static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
3765
3766
	// Attempt to prevent a recursive loop.
3767
	++$level;
3768
	if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
3769
		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...
3770
	if ($from_fatal_error)
3771
		$has_fatal_error = true;
3772
3773
	// Clear out the stat cache.
3774
	if (function_exists('trackStats'))
3775
		trackStats();
3776
3777
	// If we have mail to send, send it.
3778
	if (function_exists('AddMailQueue') && !empty($context['flush_mail']))
3779
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
3780
		AddMailQueue(true);
3781
3782
	$do_header = $header === null ? !$header_done : $header;
3783
	if ($do_footer === null)
3784
		$do_footer = $do_header;
3785
3786
	// Has the template/header been done yet?
3787
	if ($do_header)
3788
	{
3789
		// Was the page title set last minute? Also update the HTML safe one.
3790
		if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
3791
			$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](html_entity_decode($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3792
3793
		// Start up the session URL fixer.
3794
		ob_start('ob_sessrewrite');
3795
3796
		if (!empty($settings['output_buffers']) && is_string($settings['output_buffers']))
3797
			$buffers = explode(',', $settings['output_buffers']);
3798
		elseif (!empty($settings['output_buffers']))
3799
			$buffers = $settings['output_buffers'];
3800
		else
3801
			$buffers = array();
3802
3803
		if (isset($modSettings['integrate_buffer']))
3804
			$buffers = array_merge(explode(',', $modSettings['integrate_buffer']), $buffers);
3805
3806
		if (!empty($buffers))
3807
			foreach ($buffers as $function)
3808
			{
3809
				$call = call_helper($function, true);
3810
3811
				// Is it valid?
3812
				if (!empty($call))
3813
					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

3813
					ob_start(/** @scrutinizer ignore-type */ $call);
Loading history...
3814
			}
3815
3816
		// Display the screen in the logical order.
3817
		template_header();
3818
		$header_done = true;
3819
	}
3820
	if ($do_footer)
3821
	{
3822
		loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main');
3823
3824
		// Anything special to put out?
3825
		if (!empty($context['insert_after_template']) && !isset($_REQUEST['xml']))
3826
			echo $context['insert_after_template'];
3827
3828
		// Just so we don't get caught in an endless loop of errors from the footer...
3829
		if (!$footer_done)
3830
		{
3831
			$footer_done = true;
3832
			template_footer();
3833
3834
			// (since this is just debugging... it's okay that it's after </html>.)
3835
			if (!isset($_REQUEST['xml']))
3836
				displayDebug();
3837
		}
3838
	}
3839
3840
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
3841
	if ($should_log)
3842
		$_SESSION['old_url'] = $_SERVER['REQUEST_URL'];
3843
3844
	// For session check verification.... don't switch browsers...
3845
	$_SESSION['USER_AGENT'] = empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT'];
3846
3847
	// Hand off the output to the portal, etc. we're integrated with.
3848
	call_integration_hook('integrate_exit', array($do_footer));
3849
3850
	// Don't exit if we're coming from index.php; that will pass through normally.
3851
	if (!$from_index)
3852
		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...
3853
}
3854
3855
/**
3856
 * Get the size of a specified image with better error handling.
3857
 *
3858
 * @todo see if it's better in Subs-Graphics, but one step at the time.
3859
 * Uses getimagesize() to determine the size of a file.
3860
 * Attempts to connect to the server first so it won't time out.
3861
 *
3862
 * @param string $url The URL of the image
3863
 * @return array|false The image size as array (width, height), or false on failure
3864
 */
3865
function url_image_size($url)
3866
{
3867
	global $sourcedir;
3868
3869
	// Make sure it is a proper URL.
3870
	$url = str_replace(' ', '%20', $url);
3871
3872
	// Can we pull this from the cache... please please?
3873
	if (($temp = cache_get_data('url_image_size-' . md5($url), 240)) !== null)
3874
		return $temp;
3875
	$t = microtime(true);
3876
3877
	// Get the host to pester...
3878
	preg_match('~^\w+://(.+?)/(.*)$~', $url, $match);
3879
3880
	// Can't figure it out, just try the image size.
3881
	if ($url == '' || $url == 'http://' || $url == 'https://')
3882
	{
3883
		return false;
3884
	}
3885
	elseif (!isset($match[1]))
3886
	{
3887
		$size = @getimagesize($url);
3888
	}
3889
	else
3890
	{
3891
		// Try to connect to the server... give it half a second.
3892
		$temp = 0;
3893
		$fp = @fsockopen($match[1], 80, $temp, $temp, 0.5);
3894
3895
		// Successful?  Continue...
3896
		if ($fp != false)
3897
		{
3898
			// Send the HEAD request (since we don't have to worry about chunked, HTTP/1.1 is fine here.)
3899
			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");
3900
3901
			// Read in the HTTP/1.1 or whatever.
3902
			$test = substr(fgets($fp, 11), -1);
3903
			fclose($fp);
3904
3905
			// See if it returned a 404/403 or something.
3906
			if ($test < 4)
3907
			{
3908
				$size = @getimagesize($url);
3909
3910
				// This probably means allow_url_fopen is off, let's try GD.
3911
				if ($size === false && function_exists('imagecreatefromstring'))
3912
				{
3913
					// It's going to hate us for doing this, but another request...
3914
					$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

3914
					$image = @imagecreatefromstring(/** @scrutinizer ignore-type */ fetch_web_data($url));
Loading history...
3915
					if ($image !== false)
3916
					{
3917
						$size = array(imagesx($image), imagesy($image));
3918
						imagedestroy($image);
3919
					}
3920
				}
3921
			}
3922
		}
3923
	}
3924
3925
	// If we didn't get it, we failed.
3926
	if (!isset($size))
3927
		$size = false;
3928
3929
	// If this took a long time, we may never have to do it again, but then again we might...
3930
	if (microtime(true) - $t > 0.8)
3931
		cache_put_data('url_image_size-' . md5($url), $size, 240);
3932
3933
	// Didn't work.
3934
	return $size;
3935
}
3936
3937
/**
3938
 * Sets up the basic theme context stuff.
3939
 *
3940
 * @param bool $forceload Whether to load the theme even if it's already loaded
3941
 */
3942
function setupThemeContext($forceload = false)
3943
{
3944
	global $modSettings, $user_info, $scripturl, $context, $settings, $options, $txt, $maintenance;
3945
	global $smcFunc;
3946
	static $loaded = false;
3947
3948
	// Under SSI this function can be called more then once.  That can cause some problems.
3949
	//   So only run the function once unless we are forced to run it again.
3950
	if ($loaded && !$forceload)
3951
		return;
3952
3953
	$loaded = true;
3954
3955
	$context['in_maintenance'] = !empty($maintenance);
3956
	$context['current_time'] = timeformat(time(), false);
3957
	$context['current_action'] = isset($_GET['action']) ? $smcFunc['htmlspecialchars']($_GET['action']) : '';
3958
	$context['random_news_line'] = array();
3959
3960
	// Get some news...
3961
	$context['news_lines'] = array_filter(explode("\n", str_replace("\r", '', trim(addslashes($modSettings['news'])))));
3962
	for ($i = 0, $n = count($context['news_lines']); $i < $n; $i++)
3963
	{
3964
		if (trim($context['news_lines'][$i]) == '')
3965
			continue;
3966
3967
		// Clean it up for presentation ;).
3968
		$context['news_lines'][$i] = parse_bbc(stripslashes(trim($context['news_lines'][$i])), true, 'news' . $i);
3969
	}
3970
3971
	if (!empty($context['news_lines']) && (!empty($modSettings['allow_guestAccess']) || $context['user']['is_logged']))
3972
		$context['random_news_line'] = $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)];
3973
3974
	if (!$user_info['is_guest'])
3975
	{
3976
		$context['user']['messages'] = &$user_info['messages'];
3977
		$context['user']['unread_messages'] = &$user_info['unread_messages'];
3978
		$context['user']['alerts'] = &$user_info['alerts'];
3979
3980
		// Personal message popup...
3981
		if ($user_info['unread_messages'] > (isset($_SESSION['unread_messages']) ? $_SESSION['unread_messages'] : 0))
3982
			$context['user']['popup_messages'] = true;
3983
		else
3984
			$context['user']['popup_messages'] = false;
3985
		$_SESSION['unread_messages'] = $user_info['unread_messages'];
3986
3987
		if (allowedTo('moderate_forum'))
3988
			$context['unapproved_members'] = !empty($modSettings['unapprovedMembers']) ? $modSettings['unapprovedMembers'] : 0;
3989
3990
		$context['user']['avatar'] = set_avatar_data(array(
3991
			'filename' => $user_info['avatar']['filename'],
3992
			'avatar' => $user_info['avatar']['url'],
3993
			'email' => $user_info['email'],
3994
		));
3995
3996
		// Figure out how long they've been logged in.
3997
		$context['user']['total_time_logged_in'] = array(
3998
			'days' => floor($user_info['total_time_logged_in'] / 86400),
3999
			'hours' => floor(($user_info['total_time_logged_in'] % 86400) / 3600),
4000
			'minutes' => floor(($user_info['total_time_logged_in'] % 3600) / 60)
4001
		);
4002
	}
4003
	else
4004
	{
4005
		$context['user']['messages'] = 0;
4006
		$context['user']['unread_messages'] = 0;
4007
		$context['user']['avatar'] = array();
4008
		$context['user']['total_time_logged_in'] = array('days' => 0, 'hours' => 0, 'minutes' => 0);
4009
		$context['user']['popup_messages'] = false;
4010
4011
		// If we've upgraded recently, go easy on the passwords.
4012
		if (!empty($modSettings['disableHashTime']) && ($modSettings['disableHashTime'] == 1 || time() < $modSettings['disableHashTime']))
4013
			$context['disable_login_hashing'] = true;
4014
	}
4015
4016
	// Setup the main menu items.
4017
	setupMenuContext();
4018
4019
	// This is here because old index templates might still use it.
4020
	$context['show_news'] = !empty($settings['enable_news']);
4021
4022
	// This is done to allow theme authors to customize it as they want.
4023
	$context['show_pm_popup'] = $context['user']['popup_messages'] && !empty($options['popup_messages']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != 'pm');
4024
4025
	// 2.1+: Add the PM popup here instead. Theme authors can still override it simply by editing/removing the 'fPmPopup' in the array.
4026
	if ($context['show_pm_popup'])
4027
		addInlineJavaScript('
4028
		jQuery(document).ready(function($) {
4029
			new smc_Popup({
4030
				heading: ' . JavaScriptEscape($txt['show_personal_messages_heading']) . ',
4031
				content: ' . JavaScriptEscape(sprintf($txt['show_personal_messages'], $context['user']['unread_messages'], $scripturl . '?action=pm')) . ',
4032
				icon_class: \'main_icons mail_new\'
4033
			});
4034
		});');
4035
4036
	// Add a generic "Are you sure?" confirmation message.
4037
	addInlineJavaScript('
4038
	var smf_you_sure =' . JavaScriptEscape($txt['quickmod_confirm']) . ';');
4039
4040
	// Now add the capping code for avatars.
4041
	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')
4042
		addInlineCss('
4043
	img.avatar { max-width: ' . $modSettings['avatar_max_width_external'] . 'px !important; max-height: ' . $modSettings['avatar_max_height_external'] . 'px !important; }');
4044
4045
	// Add max image limits
4046
	if (!empty($modSettings['max_image_width']))
4047
		addInlineCss('
4048
	.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); }');
4049
4050
	if (!empty($modSettings['max_image_height']))
4051
		addInlineCss('
4052
	.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; }');
4053
4054
	// This looks weird, but it's because BoardIndex.php references the variable.
4055
	$context['common_stats']['latest_member'] = array(
4056
		'id' => $modSettings['latestMember'],
4057
		'name' => $modSettings['latestRealName'],
4058
		'href' => $scripturl . '?action=profile;u=' . $modSettings['latestMember'],
4059
		'link' => '<a href="' . $scripturl . '?action=profile;u=' . $modSettings['latestMember'] . '">' . $modSettings['latestRealName'] . '</a>',
4060
	);
4061
	$context['common_stats'] = array(
4062
		'total_posts' => comma_format($modSettings['totalMessages']),
4063
		'total_topics' => comma_format($modSettings['totalTopics']),
4064
		'total_members' => comma_format($modSettings['totalMembers']),
4065
		'latest_member' => $context['common_stats']['latest_member'],
4066
	);
4067
	$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']);
4068
4069
	if (empty($settings['theme_version']))
4070
		addJavaScriptVar('smf_scripturl', $scripturl);
4071
4072
	if (!isset($context['page_title']))
4073
		$context['page_title'] = '';
4074
4075
	// Set some specific vars.
4076
	$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](html_entity_decode($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
4077
	$context['meta_keywords'] = !empty($modSettings['meta_keywords']) ? $smcFunc['htmlspecialchars']($modSettings['meta_keywords']) : '';
4078
4079
	// Content related meta tags, including Open Graph
4080
	$context['meta_tags'][] = array('property' => 'og:site_name', 'content' => $context['forum_name']);
4081
	$context['meta_tags'][] = array('property' => 'og:title', 'content' => $context['page_title_html_safe']);
4082
4083
	if (!empty($context['meta_keywords']))
4084
		$context['meta_tags'][] = array('name' => 'keywords', 'content' => $context['meta_keywords']);
4085
4086
	if (!empty($context['canonical_url']))
4087
		$context['meta_tags'][] = array('property' => 'og:url', 'content' => $context['canonical_url']);
4088
4089
	if (!empty($settings['og_image']))
4090
		$context['meta_tags'][] = array('property' => 'og:image', 'content' => $settings['og_image']);
4091
4092
	if (!empty($context['meta_description']))
4093
	{
4094
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['meta_description']);
4095
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['meta_description']);
4096
	}
4097
	else
4098
	{
4099
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['page_title_html_safe']);
4100
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['page_title_html_safe']);
4101
	}
4102
4103
	call_integration_hook('integrate_theme_context');
4104
}
4105
4106
/**
4107
 * Helper function to set the system memory to a needed value
4108
 * - If the needed memory is greater than current, will attempt to get more
4109
 * - if in_use is set to true, will also try to take the current memory usage in to account
4110
 *
4111
 * @param string $needed The amount of memory to request, if needed, like 256M
4112
 * @param bool $in_use Set to true to account for current memory usage of the script
4113
 * @return boolean True if we have at least the needed memory
4114
 */
4115
function setMemoryLimit($needed, $in_use = false)
4116
{
4117
	// everything in bytes
4118
	$memory_current = memoryReturnBytes(ini_get('memory_limit'));
4119
	$memory_needed = memoryReturnBytes($needed);
4120
4121
	// should we account for how much is currently being used?
4122
	if ($in_use)
4123
		$memory_needed += function_exists('memory_get_usage') ? memory_get_usage() : (2 * 1048576);
4124
4125
	// if more is needed, request it
4126
	if ($memory_current < $memory_needed)
4127
	{
4128
		@ini_set('memory_limit', ceil($memory_needed / 1048576) . 'M');
4129
		$memory_current = memoryReturnBytes(ini_get('memory_limit'));
4130
	}
4131
4132
	$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

4132
	$memory_current = max($memory_current, memoryReturnBytes(/** @scrutinizer ignore-type */ get_cfg_var('memory_limit')));
Loading history...
4133
4134
	// return success or not
4135
	return (bool) ($memory_current >= $memory_needed);
4136
}
4137
4138
/**
4139
 * Helper function to convert memory string settings to bytes
4140
 *
4141
 * @param string $val The byte string, like 256M or 1G
4142
 * @return integer The string converted to a proper integer in bytes
4143
 */
4144
function memoryReturnBytes($val)
4145
{
4146
	if (is_integer($val))
0 ignored issues
show
introduced by
The condition is_integer($val) is always false.
Loading history...
4147
		return $val;
4148
4149
	// Separate the number from the designator
4150
	$val = trim($val);
4151
	$num = intval(substr($val, 0, strlen($val) - 1));
4152
	$last = strtolower(substr($val, -1));
4153
4154
	// convert to bytes
4155
	switch ($last)
4156
	{
4157
		case 'g':
4158
			$num *= 1024;
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
4159
		case 'm':
4160
			$num *= 1024;
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
4161
		case 'k':
4162
			$num *= 1024;
4163
	}
4164
	return $num;
4165
}
4166
4167
/**
4168
 * The header template
4169
 */
4170
function template_header()
4171
{
4172
	global $txt, $modSettings, $context, $user_info, $boarddir, $cachedir, $cache_enable, $language;
4173
4174
	setupThemeContext();
4175
4176
	// Print stuff to prevent caching of pages (except on attachment errors, etc.)
4177
	if (empty($context['no_last_modified']))
4178
	{
4179
		header('expires: Mon, 26 Jul 1997 05:00:00 GMT');
4180
		header('last-modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
4181
4182
		// Are we debugging the template/html content?
4183
		if (!isset($_REQUEST['xml']) && isset($_GET['debug']) && !isBrowser('ie'))
4184
			header('content-type: application/xhtml+xml');
4185
		elseif (!isset($_REQUEST['xml']))
4186
			header('content-type: text/html; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
4187
	}
4188
4189
	header('content-type: text/' . (isset($_REQUEST['xml']) ? 'xml' : 'html') . '; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
4190
4191
	// We need to splice this in after the body layer, or after the main layer for older stuff.
4192
	if ($context['in_maintenance'] && $context['user']['is_admin'])
4193
	{
4194
		$position = array_search('body', $context['template_layers']);
4195
		if ($position === false)
4196
			$position = array_search('main', $context['template_layers']);
4197
4198
		if ($position !== false)
4199
		{
4200
			$before = array_slice($context['template_layers'], 0, $position + 1);
4201
			$after = array_slice($context['template_layers'], $position + 1);
4202
			$context['template_layers'] = array_merge($before, array('maint_warning'), $after);
4203
		}
4204
	}
4205
4206
	$checked_securityFiles = false;
4207
	$showed_banned = false;
4208
	foreach ($context['template_layers'] as $layer)
4209
	{
4210
		loadSubTemplate($layer . '_above', true);
4211
4212
		// May seem contrived, but this is done in case the body and main layer aren't there...
4213
		if (in_array($layer, array('body', 'main')) && allowedTo('admin_forum') && !$user_info['is_guest'] && !$checked_securityFiles)
4214
		{
4215
			$checked_securityFiles = true;
4216
4217
			$securityFiles = array('install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~');
4218
4219
			// Add your own files.
4220
			call_integration_hook('integrate_security_files', array(&$securityFiles));
4221
4222
			foreach ($securityFiles as $i => $securityFile)
4223
			{
4224
				if (!file_exists($boarddir . '/' . $securityFile))
4225
					unset($securityFiles[$i]);
4226
			}
4227
4228
			// We are already checking so many files...just few more doesn't make any difference! :P
4229
			if (!empty($modSettings['currentAttachmentUploadDir']))
4230
				$path = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
4231
4232
			else
4233
				$path = $modSettings['attachmentUploadDir'];
4234
4235
			secureDirectory($path, true);
4236
			secureDirectory($cachedir);
4237
4238
			// If agreement is enabled, at least the english version shall exist
4239
			if (!empty($modSettings['requireAgreement']))
4240
				$agreement = !file_exists($boarddir . '/agreement.txt');
4241
4242
			// If privacy policy is enabled, at least the default language version shall exist
4243
			if (!empty($modSettings['requirePolicyAgreement']))
4244
				$policy_agreement = empty($modSettings['policy_' . $language]);
4245
4246
			if (!empty($securityFiles) ||
4247
				(!empty($cache_enable) && !is_writable($cachedir)) ||
4248
				!empty($agreement) ||
4249
				!empty($policy_agreement) ||
4250
				!empty($context['auth_secret_missing']))
4251
			{
4252
				echo '
4253
		<div class="errorbox">
4254
			<p class="alert">!!</p>
4255
			<h3>', empty($securityFiles) && empty($context['auth_secret_missing']) ? $txt['generic_warning'] : $txt['security_risk'], '</h3>
4256
			<p>';
4257
4258
				foreach ($securityFiles as $securityFile)
4259
				{
4260
					echo '
4261
				', $txt['not_removed'], '<strong>', $securityFile, '</strong>!<br>';
4262
4263
					if ($securityFile == 'Settings.php~' || $securityFile == 'Settings_bak.php~')
4264
						echo '
4265
				', sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)), '<br>';
4266
				}
4267
4268
				if (!empty($cache_enable) && !is_writable($cachedir))
4269
					echo '
4270
				<strong>', $txt['cache_writable'], '</strong><br>';
4271
4272
				if (!empty($agreement))
4273
					echo '
4274
				<strong>', $txt['agreement_missing'], '</strong><br>';
4275
4276
				if (!empty($policy_agreement))
4277
					echo '
4278
				<strong>', $txt['policy_agreement_missing'], '</strong><br>';
4279
4280
				if (!empty($context['auth_secret_missing']))
4281
					echo '
4282
				<strong>', $txt['auth_secret_missing'], '</strong><br>';
4283
4284
				echo '
4285
			</p>
4286
		</div>';
4287
			}
4288
		}
4289
		// If the user is banned from posting inform them of it.
4290
		elseif (in_array($layer, array('main', 'body')) && isset($_SESSION['ban']['cannot_post']) && !$showed_banned)
4291
		{
4292
			$showed_banned = true;
4293
			echo '
4294
				<div class="windowbg alert" style="margin: 2ex; padding: 2ex; border: 2px dashed red;">
4295
					', sprintf($txt['you_are_post_banned'], $user_info['is_guest'] ? $txt['guest_title'] : $user_info['name']);
4296
4297
			if (!empty($_SESSION['ban']['cannot_post']['reason']))
4298
				echo '
4299
					<div style="padding-left: 4ex; padding-top: 1ex;">', $_SESSION['ban']['cannot_post']['reason'], '</div>';
4300
4301
			if (!empty($_SESSION['ban']['expire_time']))
4302
				echo '
4303
					<div>', sprintf($txt['your_ban_expires'], timeformat($_SESSION['ban']['expire_time'], false)), '</div>';
4304
			else
4305
				echo '
4306
					<div>', $txt['your_ban_expires_never'], '</div>';
4307
4308
			echo '
4309
				</div>';
4310
		}
4311
	}
4312
}
4313
4314
/**
4315
 * Show the copyright.
4316
 */
4317
function theme_copyright()
4318
{
4319
	global $forum_copyright, $scripturl;
4320
4321
	// Don't display copyright for things like SSI.
4322
	if (SMF !== 1)
0 ignored issues
show
introduced by
The condition SMF !== 1 is always true.
Loading history...
4323
		return;
4324
4325
	// Put in the version...
4326
	printf($forum_copyright, SMF_FULL_VERSION, SMF_SOFTWARE_YEAR, $scripturl);
4327
}
4328
4329
/**
4330
 * The template footer
4331
 */
4332
function template_footer()
4333
{
4334
	global $context, $modSettings, $db_count;
4335
4336
	// Show the load time?  (only makes sense for the footer.)
4337
	$context['show_load_time'] = !empty($modSettings['timeLoadPageEnable']);
4338
	$context['load_time'] = round(microtime(true) - TIME_START, 3);
4339
	$context['load_queries'] = $db_count;
4340
4341
	if (!empty($context['template_layers']) && is_array($context['template_layers']))
4342
		foreach (array_reverse($context['template_layers']) as $layer)
4343
			loadSubTemplate($layer . '_below', true);
4344
}
4345
4346
/**
4347
 * Output the Javascript files
4348
 * 	- tabbing in this function is to make the HTML source look good and proper
4349
 *  - if deferred is set function will output all JS set to load at page end
4350
 *
4351
 * @param bool $do_deferred If true will only output the deferred JS (the stuff that goes right before the closing body tag)
4352
 */
4353
function template_javascript($do_deferred = false)
4354
{
4355
	global $context, $modSettings, $settings;
4356
4357
	// Use this hook to minify/optimize Javascript files and vars
4358
	call_integration_hook('integrate_pre_javascript_output', array(&$do_deferred));
4359
4360
	$toMinify = array(
4361
		'standard' => array(),
4362
		'defer' => array(),
4363
		'async' => array(),
4364
	);
4365
4366
	// Ouput the declared Javascript variables.
4367
	if (!empty($context['javascript_vars']) && !$do_deferred)
4368
	{
4369
		echo '
4370
	<script>';
4371
4372
		foreach ($context['javascript_vars'] as $key => $value)
4373
		{
4374
			if (!is_string($key) || is_numeric($key))
4375
				continue;
4376
4377
			if (!is_string($value) && !is_numeric($value))
4378
				$value = null;
4379
4380
			echo "\n\t\t", 'var ', $key, isset($value) ? ' = ' . $value : '', ';';
4381
		}
4382
4383
		echo '
4384
	</script>';
4385
	}
4386
4387
	// In the dark days before HTML5, deferred JS files needed to be loaded at the end of the body.
4388
	// Now we load them in the head and use 'async' and/or 'defer' attributes. Much better performance.
4389
	if (!$do_deferred)
4390
	{
4391
		// While we have JavaScript files to place in the template.
4392
		foreach ($context['javascript_files'] as $id => $js_file)
4393
		{
4394
			// Last minute call! allow theme authors to disable single files.
4395
			if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
4396
				continue;
4397
4398
			// By default files don't get minimized unless the file explicitly says so!
4399
			if (!empty($js_file['options']['minimize']) && !empty($modSettings['minimize_files']))
4400
			{
4401
				if (!empty($js_file['options']['async']))
4402
					$toMinify['async'][] = $js_file;
4403
4404
				elseif (!empty($js_file['options']['defer']))
4405
					$toMinify['defer'][] = $js_file;
4406
4407
				else
4408
					$toMinify['standard'][] = $js_file;
4409
4410
				// Grab a random seed.
4411
				if (!isset($minSeed) && isset($js_file['options']['seed']))
4412
					$minSeed = $js_file['options']['seed'];
4413
			}
4414
4415
			else
4416
			{
4417
				echo '
4418
	<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' : '';
4419
4420
				if (!empty($js_file['options']['attributes']))
4421
					foreach ($js_file['options']['attributes'] as $key => $value)
4422
					{
4423
						if (is_bool($value))
4424
							echo !empty($value) ? ' ' . $key : '';
4425
4426
						else
4427
							echo ' ', $key, '="', $value, '"';
4428
					}
4429
4430
				echo '></script>';
4431
			}
4432
		}
4433
4434
		foreach ($toMinify as $js_files)
4435
		{
4436
			if (!empty($js_files))
4437
			{
4438
				$result = custMinify($js_files, 'js');
4439
4440
				$minSuccessful = array_keys($result) === array('smf_minified');
4441
4442
				foreach ($result as $minFile)
4443
					echo '
4444
	<script src="', $minFile['fileUrl'], $minSuccessful && isset($minSeed) ? $minSeed : '', '"', !empty($minFile['options']['async']) ? ' async' : '', !empty($minFile['options']['defer']) ? ' defer' : '', '></script>';
4445
			}
4446
		}
4447
	}
4448
4449
	// Inline JavaScript - Actually useful some times!
4450
	if (!empty($context['javascript_inline']))
4451
	{
4452
		if (!empty($context['javascript_inline']['defer']) && $do_deferred)
4453
		{
4454
			echo '
4455
<script>
4456
window.addEventListener("DOMContentLoaded", function() {';
4457
4458
			foreach ($context['javascript_inline']['defer'] as $js_code)
4459
				echo $js_code;
4460
4461
			echo '
4462
});
4463
</script>';
4464
		}
4465
4466
		if (!empty($context['javascript_inline']['standard']) && !$do_deferred)
4467
		{
4468
			echo '
4469
	<script>';
4470
4471
			foreach ($context['javascript_inline']['standard'] as $js_code)
4472
				echo $js_code;
4473
4474
			echo '
4475
	</script>';
4476
		}
4477
	}
4478
}
4479
4480
/**
4481
 * Output the CSS files
4482
 */
4483
function template_css()
4484
{
4485
	global $context, $db_show_debug, $boardurl, $settings, $modSettings;
4486
4487
	// Use this hook to minify/optimize CSS files
4488
	call_integration_hook('integrate_pre_css_output');
4489
4490
	$toMinify = array();
4491
	$normal = array();
4492
4493
	uasort(
4494
		$context['css_files'],
4495
		function ($a, $b)
4496
		{
4497
			return $a['options']['order_pos'] < $b['options']['order_pos'] ? -1 : ($a['options']['order_pos'] > $b['options']['order_pos'] ? 1 : 0);
4498
		}
4499
	);
4500
4501
	foreach ($context['css_files'] as $id => $file)
4502
	{
4503
		// Last minute call! allow theme authors to disable single files.
4504
		if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
4505
			continue;
4506
4507
		// Files are minimized unless they explicitly opt out.
4508
		if (!isset($file['options']['minimize']))
4509
			$file['options']['minimize'] = true;
4510
4511
		if (!empty($file['options']['minimize']) && !empty($modSettings['minimize_files']) && !isset($_REQUEST['normalcss']))
4512
		{
4513
			$toMinify[] = $file;
4514
4515
			// Grab a random seed.
4516
			if (!isset($minSeed) && isset($file['options']['seed']))
4517
				$minSeed = $file['options']['seed'];
4518
		}
4519
		else
4520
			$normal[] = array(
4521
				'url' => $file['fileUrl'] . (isset($file['options']['seed']) ? $file['options']['seed'] : ''),
4522
				'attributes' => !empty($file['options']['attributes']) ? $file['options']['attributes'] : array()
4523
			);
4524
	}
4525
4526
	if (!empty($toMinify))
4527
	{
4528
		$result = custMinify($toMinify, 'css');
4529
4530
		$minSuccessful = array_keys($result) === array('smf_minified');
4531
4532
		foreach ($result as $minFile)
4533
			echo '
4534
	<link rel="stylesheet" href="', $minFile['fileUrl'], $minSuccessful && isset($minSeed) ? $minSeed : '', '">';
4535
	}
4536
4537
	// Print the rest after the minified files.
4538
	if (!empty($normal))
4539
		foreach ($normal as $nf)
4540
		{
4541
			echo '
4542
	<link rel="stylesheet" href="', $nf['url'], '"';
4543
4544
			if (!empty($nf['attributes']))
4545
				foreach ($nf['attributes'] as $key => $value)
4546
				{
4547
					if (is_bool($value))
4548
						echo !empty($value) ? ' ' . $key : '';
4549
					else
4550
						echo ' ', $key, '="', $value, '"';
4551
				}
4552
4553
			echo '>';
4554
		}
4555
4556
	if ($db_show_debug === true)
4557
	{
4558
		// Try to keep only what's useful.
4559
		$repl = array($boardurl . '/Themes/' => '', $boardurl . '/' => '');
4560
		foreach ($context['css_files'] as $file)
4561
			$context['debug']['sheets'][] = strtr($file['fileName'], $repl);
4562
	}
4563
4564
	if (!empty($context['css_header']))
4565
	{
4566
		echo '
4567
	<style>';
4568
4569
		foreach ($context['css_header'] as $css)
4570
			echo $css . '
4571
	';
4572
4573
		echo '
4574
	</style>';
4575
	}
4576
}
4577
4578
/**
4579
 * Get an array of previously defined files and adds them to our main minified files.
4580
 * Sets a one day cache to avoid re-creating a file on every request.
4581
 *
4582
 * @param array $data The files to minify.
4583
 * @param string $type either css or js.
4584
 * @return array Info about the minified file, or about the original files if the minify process failed.
4585
 */
4586
function custMinify($data, $type)
4587
{
4588
	global $settings, $txt;
4589
4590
	$types = array('css', 'js');
4591
	$type = !empty($type) && in_array($type, $types) ? $type : false;
4592
	$data = is_array($data) ? $data : array();
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
4593
4594
	if (empty($type) || empty($data))
4595
		return $data;
4596
4597
	// Different pages include different files, so we use a hash to label the different combinations
4598
	$hash = md5(implode(' ', array_map(
4599
		function($file)
4600
		{
4601
			return $file['filePath'] . '-' . $file['mtime'];
4602
		},
4603
		$data
4604
	)));
4605
4606
	// Is this a deferred or asynchronous JavaScript file?
4607
	$async = $type === 'js';
4608
	$defer = $type === 'js';
4609
	if ($type === 'js')
4610
	{
4611
		foreach ($data as $id => $file)
4612
		{
4613
			// A minified script should only be loaded asynchronously if all its components wanted to be.
4614
			if (empty($file['options']['async']))
4615
				$async = false;
4616
4617
			// A minified script should only be deferred if all its components wanted to be.
4618
			if (empty($file['options']['defer']))
4619
				$defer = false;
4620
		}
4621
	}
4622
4623
	// Did we already do this?
4624
	$minified_file = $settings['theme_dir'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/minified_' . $hash . '.' . $type;
4625
	$already_exists = file_exists($minified_file);
4626
4627
	// Already done?
4628
	if ($already_exists)
4629
	{
4630
		return array('smf_minified' => array(
4631
			'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file),
4632
			'filePath' => $minified_file,
4633
			'fileName' => basename($minified_file),
4634
			'options' => array('async' => !empty($async), 'defer' => !empty($defer)),
4635
		));
4636
	}
4637
	// File has to exist. If it doesn't, try to create it.
4638
	elseif (@fopen($minified_file, 'w') === false || !smf_chmod($minified_file))
4639
	{
4640
		loadLanguage('Errors');
4641
		log_error(sprintf($txt['file_not_created'], $minified_file), 'general');
4642
4643
		// The process failed, so roll back to print each individual file.
4644
		return $data;
4645
	}
4646
4647
	// No namespaces, sorry!
4648
	$classType = 'MatthiasMullie\\Minify\\' . strtoupper($type);
4649
4650
	$minifier = new $classType();
4651
4652
	foreach ($data as $id => $file)
4653
	{
4654
		$toAdd = !empty($file['filePath']) && file_exists($file['filePath']) ? $file['filePath'] : false;
4655
4656
		// The file couldn't be located so it won't be added. Log this error.
4657
		if (empty($toAdd))
4658
		{
4659
			loadLanguage('Errors');
4660
			log_error(sprintf($txt['file_minimize_fail'], !empty($file['fileName']) ? $file['fileName'] : $id), 'general');
4661
			continue;
4662
		}
4663
4664
		// Add this file to the list.
4665
		$minifier->add($toAdd);
4666
	}
4667
4668
	// Create the file.
4669
	$minifier->minify($minified_file);
4670
	unset($minifier);
4671
	clearstatcache();
4672
4673
	// Minify process failed.
4674
	if (!filesize($minified_file))
4675
	{
4676
		loadLanguage('Errors');
4677
		log_error(sprintf($txt['file_not_created'], $minified_file), 'general');
4678
4679
		// The process failed so roll back to print each individual file.
4680
		return $data;
4681
	}
4682
4683
	return array('smf_minified' => array(
4684
		'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file),
4685
		'filePath' => $minified_file,
4686
		'fileName' => basename($minified_file),
4687
		'options' => array('async' => $async, 'defer' => $defer),
4688
	));
4689
}
4690
4691
/**
4692
 * Clears out old minimized CSS and JavaScript files and ensures $modSettings['browser_cache'] is up to date
4693
 */
4694
function deleteAllMinified()
4695
{
4696
	global $smcFunc, $txt, $modSettings;
4697
4698
	$not_deleted = array();
4699
	$most_recent = 0;
4700
4701
	// Kinda sucks that we need to do another query to get all the theme dirs, but c'est la vie.
4702
	$request = $smcFunc['db_query']('', '
4703
		SELECT id_theme AS id, value AS dir
4704
		FROM {db_prefix}themes
4705
		WHERE variable = {string:var}',
4706
		array(
4707
			'var' => 'theme_dir',
4708
		)
4709
	);
4710
	while ($theme = $smcFunc['db_fetch_assoc']($request))
4711
	{
4712
		foreach (array('css', 'js') as $type)
4713
		{
4714
			foreach (glob(rtrim($theme['dir'], '/') . '/' . ($type == 'css' ? 'css' : 'scripts') . '/*.' . $type) as $filename)
4715
			{
4716
				// We want to find the most recent mtime of non-minified files
4717
				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

4717
				if (strpos(/** @scrutinizer ignore-type */ pathinfo($filename, PATHINFO_BASENAME), 'minified') === false)
Loading history...
4718
					$most_recent = max($modSettings['browser_cache'], (int) @filemtime($filename));
4719
4720
				// Try to delete minified files. Add them to our error list if that fails.
4721
				elseif (!@unlink($filename))
4722
					$not_deleted[] = $filename;
4723
			}
4724
		}
4725
	}
4726
	$smcFunc['db_free_result']($request);
4727
4728
	// This setting tracks the most recent modification time of any of our CSS and JS files
4729
	if ($most_recent > $modSettings['browser_cache'])
4730
		updateSettings(array('browser_cache' => $most_recent));
4731
4732
	// If any of the files could not be deleted, log an error about it.
4733
	if (!empty($not_deleted))
4734
	{
4735
		loadLanguage('Errors');
4736
		log_error(sprintf($txt['unlink_minimized_fail'], implode('<br>', $not_deleted)), 'general');
4737
	}
4738
}
4739
4740
/**
4741
 * Get an attachment's encrypted filename. If $new is true, won't check for file existence.
4742
 *
4743
 * @todo this currently returns the hash if new, and the full filename otherwise.
4744
 * Something messy like that.
4745
 * @todo and of course everything relies on this behavior and work around it. :P.
4746
 * Converters included.
4747
 *
4748
 * @param string $filename The name of the file
4749
 * @param int $attachment_id The ID of the attachment
4750
 * @param string|null $dir Which directory it should be in (null to use current one)
4751
 * @param bool $new Whether this is a new attachment
4752
 * @param string $file_hash The file hash
4753
 * @return string The path to the file
4754
 */
4755
function getAttachmentFilename($filename, $attachment_id, $dir = null, $new = false, $file_hash = '')
4756
{
4757
	global $modSettings, $smcFunc;
4758
4759
	// Just make up a nice hash...
4760
	if ($new)
4761
		return sha1(md5($filename . time()) . mt_rand());
4762
4763
	// Just make sure that attachment id is only a int
4764
	$attachment_id = (int) $attachment_id;
4765
4766
	// Grab the file hash if it wasn't added.
4767
	// Left this for legacy.
4768
	if ($file_hash === '')
4769
	{
4770
		$request = $smcFunc['db_query']('', '
4771
			SELECT file_hash
4772
			FROM {db_prefix}attachments
4773
			WHERE id_attach = {int:id_attach}',
4774
			array(
4775
				'id_attach' => $attachment_id,
4776
			)
4777
		);
4778
4779
		if ($smcFunc['db_num_rows']($request) === 0)
4780
			return false;
4781
4782
		list ($file_hash) = $smcFunc['db_fetch_row']($request);
4783
		$smcFunc['db_free_result']($request);
4784
	}
4785
4786
	// Still no hash? mmm...
4787
	if (empty($file_hash))
4788
		$file_hash = sha1(md5($filename . time()) . mt_rand());
4789
4790
	// Are we using multiple directories?
4791
	if (is_array($modSettings['attachmentUploadDir']))
4792
		$path = $modSettings['attachmentUploadDir'][$dir];
4793
4794
	else
4795
		$path = $modSettings['attachmentUploadDir'];
4796
4797
	return $path . '/' . $attachment_id . '_' . $file_hash . '.dat';
4798
}
4799
4800
/**
4801
 * Convert a single IP to a ranged IP.
4802
 * internal function used to convert a user-readable format to a format suitable for the database.
4803
 *
4804
 * @param string $fullip The full IP
4805
 * @return array An array of IP parts
4806
 */
4807
function ip2range($fullip)
4808
{
4809
	// Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.)
4810
	if ($fullip == 'unknown')
4811
		$fullip = '255.255.255.255';
4812
4813
	$ip_parts = explode('-', $fullip);
4814
	$ip_array = array();
4815
4816
	// if ip 22.12.31.21
4817
	if (count($ip_parts) == 1 && isValidIP($fullip))
4818
	{
4819
		$ip_array['low'] = $fullip;
4820
		$ip_array['high'] = $fullip;
4821
		return $ip_array;
4822
	} // if ip 22.12.* -> 22.12.* - 22.12.*
4823
	elseif (count($ip_parts) == 1)
4824
	{
4825
		$ip_parts[0] = $fullip;
4826
		$ip_parts[1] = $fullip;
4827
	}
4828
4829
	// if ip 22.12.31.21-12.21.31.21
4830
	if (count($ip_parts) == 2 && isValidIP($ip_parts[0]) && isValidIP($ip_parts[1]))
4831
	{
4832
		$ip_array['low'] = $ip_parts[0];
4833
		$ip_array['high'] = $ip_parts[1];
4834
		return $ip_array;
4835
	}
4836
	elseif (count($ip_parts) == 2) // if ip 22.22.*-22.22.*
4837
	{
4838
		$valid_low = isValidIP($ip_parts[0]);
4839
		$valid_high = isValidIP($ip_parts[1]);
4840
		$count = 0;
4841
		$mode = (preg_match('/:/', $ip_parts[0]) > 0 ? ':' : '.');
4842
		$max = ($mode == ':' ? 'ffff' : '255');
4843
		$min = 0;
4844
		if (!$valid_low)
4845
		{
4846
			$ip_parts[0] = preg_replace('/\*/', '0', $ip_parts[0]);
4847
			$valid_low = isValidIP($ip_parts[0]);
4848
			while (!$valid_low)
4849
			{
4850
				$ip_parts[0] .= $mode . $min;
4851
				$valid_low = isValidIP($ip_parts[0]);
4852
				$count++;
4853
				if ($count > 9) break;
4854
			}
4855
		}
4856
4857
		$count = 0;
4858
		if (!$valid_high)
4859
		{
4860
			$ip_parts[1] = preg_replace('/\*/', $max, $ip_parts[1]);
4861
			$valid_high = isValidIP($ip_parts[1]);
4862
			while (!$valid_high)
4863
			{
4864
				$ip_parts[1] .= $mode . $max;
4865
				$valid_high = isValidIP($ip_parts[1]);
4866
				$count++;
4867
				if ($count > 9) break;
4868
			}
4869
		}
4870
4871
		if ($valid_high && $valid_low)
4872
		{
4873
			$ip_array['low'] = $ip_parts[0];
4874
			$ip_array['high'] = $ip_parts[1];
4875
		}
4876
	}
4877
4878
	return $ip_array;
4879
}
4880
4881
/**
4882
 * Lookup an IP; try shell_exec first because we can do a timeout on it.
4883
 *
4884
 * @param string $ip The IP to get the hostname from
4885
 * @return string The hostname
4886
 */
4887
function host_from_ip($ip)
4888
{
4889
	global $modSettings;
4890
4891
	if (($host = cache_get_data('hostlookup-' . $ip, 600)) !== null)
4892
		return $host;
4893
	$t = microtime(true);
4894
4895
	// Try the Linux host command, perhaps?
4896
	if (!isset($host) && (strpos(strtolower(PHP_OS), 'win') === false || strpos(strtolower(PHP_OS), 'darwin') !== false) && mt_rand(0, 1) == 1)
4897
	{
4898
		if (!isset($modSettings['host_to_dis']))
4899
			$test = @shell_exec('host -W 1 ' . @escapeshellarg($ip));
4900
		else
4901
			$test = @shell_exec('host ' . @escapeshellarg($ip));
4902
4903
		// Did host say it didn't find anything?
4904
		if (strpos($test, 'not found') !== false)
0 ignored issues
show
Bug introduced by
It seems like $test can also be of type false; 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

4904
		if (strpos(/** @scrutinizer ignore-type */ $test, 'not found') !== false)
Loading history...
4905
			$host = '';
4906
		// Invalid server option?
4907
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
4908
			updateSettings(array('host_to_dis' => 1));
4909
		// Maybe it found something, after all?
4910
		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; 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

4910
		elseif (preg_match('~\s([^\s]+?)\.\s~', /** @scrutinizer ignore-type */ $test, $match) == 1)
Loading history...
4911
			$host = $match[1];
4912
	}
4913
4914
	// This is nslookup; usually only Windows, but possibly some Unix?
4915
	if (!isset($host) && stripos(PHP_OS, 'win') !== false && strpos(strtolower(PHP_OS), 'darwin') === false && mt_rand(0, 1) == 1)
4916
	{
4917
		$test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
4918
		if (strpos($test, 'Non-existent domain') !== false)
4919
			$host = '';
4920
		elseif (preg_match('~Name:\s+([^\s]+)~', $test, $match) == 1)
4921
			$host = $match[1];
4922
	}
4923
4924
	// This is the last try :/.
4925
	if (!isset($host) || $host === false)
4926
		$host = @gethostbyaddr($ip);
4927
4928
	// It took a long time, so let's cache it!
4929
	if (microtime(true) - $t > 0.5)
4930
		cache_put_data('hostlookup-' . $ip, $host, 600);
4931
4932
	return $host;
4933
}
4934
4935
/**
4936
 * Chops a string into words and prepares them to be inserted into (or searched from) the database.
4937
 *
4938
 * @param string $text The text to split into words
4939
 * @param int $max_chars The maximum number of characters per word
4940
 * @param bool $encrypt Whether to encrypt the results
4941
 * @return array An array of ints or words depending on $encrypt
4942
 */
4943
function text2words($text, $max_chars = 20, $encrypt = false)
4944
{
4945
	global $smcFunc, $context;
4946
4947
	// Upgrader may be working on old DBs...
4948
	if (!isset($context['utf8']))
4949
		$context['utf8'] = false;
4950
4951
	// Step 1: Remove entities/things we don't consider words:
4952
	$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>' => ' ')));
4953
4954
	// Step 2: Entities we left to letters, where applicable, lowercase.
4955
	$words = un_htmlspecialchars($smcFunc['strtolower']($words));
4956
4957
	// Step 3: Ready to split apart and index!
4958
	$words = explode(' ', $words);
4959
4960
	if ($encrypt)
4961
	{
4962
		$possible_chars = array_flip(array_merge(range(46, 57), range(65, 90), range(97, 122)));
4963
		$returned_ints = array();
4964
		foreach ($words as $word)
4965
		{
4966
			if (($word = trim($word, '-_\'')) !== '')
4967
			{
4968
				$encrypted = substr(crypt($word, 'uk'), 2, $max_chars);
4969
				$total = 0;
4970
				for ($i = 0; $i < $max_chars; $i++)
4971
					$total += $possible_chars[ord($encrypted[$i])] * pow(63, $i);
4972
				$returned_ints[] = $max_chars == 4 ? min($total, 16777215) : $total;
4973
			}
4974
		}
4975
		return array_unique($returned_ints);
4976
	}
4977
	else
4978
	{
4979
		// Trim characters before and after and add slashes for database insertion.
4980
		$returned_words = array();
4981
		foreach ($words as $word)
4982
			if (($word = trim($word, '-_\'')) !== '')
4983
				$returned_words[] = $max_chars === null ? $word : substr($word, 0, $max_chars);
4984
4985
		// Filter out all words that occur more than once.
4986
		return array_unique($returned_words);
4987
	}
4988
}
4989
4990
/**
4991
 * Creates an image/text button
4992
 *
4993
 * @deprecated since 2.1
4994
 * @param string $name The name of the button (should be a main_icons class or the name of an image)
4995
 * @param string $alt The alt text
4996
 * @param string $label The $txt string to use as the label
4997
 * @param string $custom Custom text/html to add to the img tag (only when using an actual image)
4998
 * @param boolean $force_use Whether to force use of this when template_create_button is available
4999
 * @return string The HTML to display the button
5000
 */
5001
function create_button($name, $alt, $label = '', $custom = '', $force_use = false)
5002
{
5003
	global $settings, $txt;
5004
5005
	// Does the current loaded theme have this and we are not forcing the usage of this function?
5006
	if (function_exists('template_create_button') && !$force_use)
5007
		return template_create_button($name, $alt, $label = '', $custom = '');
5008
5009
	if (!$settings['use_image_buttons'])
5010
		return $txt[$alt];
5011
	elseif (!empty($settings['use_buttons']))
5012
		return '<span class="main_icons ' . $name . '" alt="' . $txt[$alt] . '"></span>' . ($label != '' ? '&nbsp;<strong>' . $txt[$label] . '</strong>' : '');
5013
	else
5014
		return '<img src="' . $settings['lang_images_url'] . '/' . $name . '" alt="' . $txt[$alt] . '" ' . $custom . '>';
5015
}
5016
5017
/**
5018
 * Sets up all of the top menu buttons
5019
 * Saves them in the cache if it is available and on
5020
 * Places the results in $context
5021
 */
5022
function setupMenuContext()
5023
{
5024
	global $context, $modSettings, $user_info, $txt, $scripturl, $sourcedir, $settings, $smcFunc, $cache_enable;
5025
5026
	// Set up the menu privileges.
5027
	$context['allow_search'] = !empty($modSettings['allow_guestAccess']) ? allowedTo('search_posts') : (!$user_info['is_guest'] && allowedTo('search_posts'));
5028
	$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'));
5029
5030
	$context['allow_memberlist'] = allowedTo('view_mlist');
5031
	$context['allow_calendar'] = allowedTo('calendar_view') && !empty($modSettings['cal_enabled']);
5032
	$context['allow_moderation_center'] = $context['user']['can_mod'];
5033
	$context['allow_pm'] = allowedTo('pm_read');
5034
5035
	$cacheTime = $modSettings['lastActive'] * 60;
5036
5037
	// Initial "can you post an event in the calendar" option - but this might have been set in the calendar already.
5038
	if (!isset($context['allow_calendar_event']))
5039
	{
5040
		$context['allow_calendar_event'] = $context['allow_calendar'] && allowedTo('calendar_post');
5041
5042
		// If you don't allow events not linked to posts and you're not an admin, we have more work to do...
5043
		if ($context['allow_calendar'] && $context['allow_calendar_event'] && empty($modSettings['cal_allow_unlinked']) && !$user_info['is_admin'])
5044
		{
5045
			$boards_can_post = boardsAllowedTo('post_new');
5046
			$context['allow_calendar_event'] &= !empty($boards_can_post);
5047
		}
5048
	}
5049
5050
	// There is some menu stuff we need to do if we're coming at this from a non-guest perspective.
5051
	if (!$context['user']['is_guest'])
5052
	{
5053
		addInlineJavaScript('
5054
	var user_menus = new smc_PopupMenu();
5055
	user_menus.add("profile", "' . $scripturl . '?action=profile;area=popup");
5056
	user_menus.add("alerts", "' . $scripturl . '?action=profile;area=alerts_popup;u=' . $context['user']['id'] . '");', true);
5057
		if ($context['allow_pm'])
5058
			addInlineJavaScript('
5059
	user_menus.add("pm", "' . $scripturl . '?action=pm;sa=popup");', true);
5060
5061
		if (!empty($modSettings['enable_ajax_alerts']))
5062
		{
5063
			require_once($sourcedir . '/Subs-Notify.php');
5064
5065
			$timeout = getNotifyPrefs($context['user']['id'], 'alert_timeout', true);
5066
			$timeout = empty($timeout) ? 10000 : $timeout[$context['user']['id']]['alert_timeout'] * 1000;
5067
5068
			addInlineJavaScript('
5069
	var new_alert_title = "' . $context['forum_name_html_safe'] . '";
5070
	var alert_timeout = ' . $timeout . ';');
5071
			loadJavaScriptFile('alerts.js', array('minimize' => true), 'smf_alerts');
5072
		}
5073
	}
5074
5075
	// All the buttons we can possible want and then some, try pulling the final list of buttons from cache first.
5076
	if (($menu_buttons = cache_get_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $cacheTime)) === null || time() - $cacheTime <= $modSettings['settings_updated'])
5077
	{
5078
		$buttons = array(
5079
			'home' => array(
5080
				'title' => $txt['home'],
5081
				'href' => $scripturl,
5082
				'show' => true,
5083
				'sub_buttons' => array(
5084
				),
5085
				'is_last' => $context['right_to_left'],
5086
			),
5087
			'search' => array(
5088
				'title' => $txt['search'],
5089
				'href' => $scripturl . '?action=search',
5090
				'show' => $context['allow_search'],
5091
				'sub_buttons' => array(
5092
				),
5093
			),
5094
			'admin' => array(
5095
				'title' => $txt['admin'],
5096
				'href' => $scripturl . '?action=admin',
5097
				'show' => $context['allow_admin'],
5098
				'sub_buttons' => array(
5099
					'featuresettings' => array(
5100
						'title' => $txt['modSettings_title'],
5101
						'href' => $scripturl . '?action=admin;area=featuresettings',
5102
						'show' => allowedTo('admin_forum'),
5103
					),
5104
					'packages' => array(
5105
						'title' => $txt['package'],
5106
						'href' => $scripturl . '?action=admin;area=packages',
5107
						'show' => allowedTo('admin_forum'),
5108
					),
5109
					'errorlog' => array(
5110
						'title' => $txt['errorlog'],
5111
						'href' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc',
5112
						'show' => allowedTo('admin_forum') && !empty($modSettings['enableErrorLogging']),
5113
					),
5114
					'permissions' => array(
5115
						'title' => $txt['edit_permissions'],
5116
						'href' => $scripturl . '?action=admin;area=permissions',
5117
						'show' => allowedTo('manage_permissions'),
5118
					),
5119
					'memberapprove' => array(
5120
						'title' => $txt['approve_members_waiting'],
5121
						'href' => $scripturl . '?action=admin;area=viewmembers;sa=browse;type=approve',
5122
						'show' => !empty($context['unapproved_members']),
5123
						'is_last' => true,
5124
					),
5125
				),
5126
			),
5127
			'moderate' => array(
5128
				'title' => $txt['moderate'],
5129
				'href' => $scripturl . '?action=moderate',
5130
				'show' => $context['allow_moderation_center'],
5131
				'sub_buttons' => array(
5132
					'modlog' => array(
5133
						'title' => $txt['modlog_view'],
5134
						'href' => $scripturl . '?action=moderate;area=modlog',
5135
						'show' => !empty($modSettings['modlog_enabled']) && !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
5136
					),
5137
					'poststopics' => array(
5138
						'title' => $txt['mc_unapproved_poststopics'],
5139
						'href' => $scripturl . '?action=moderate;area=postmod;sa=posts',
5140
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
5141
					),
5142
					'attachments' => array(
5143
						'title' => $txt['mc_unapproved_attachments'],
5144
						'href' => $scripturl . '?action=moderate;area=attachmod;sa=attachments',
5145
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
5146
					),
5147
					'reports' => array(
5148
						'title' => $txt['mc_reported_posts'],
5149
						'href' => $scripturl . '?action=moderate;area=reportedposts',
5150
						'show' => !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
5151
					),
5152
					'reported_members' => array(
5153
						'title' => $txt['mc_reported_members'],
5154
						'href' => $scripturl . '?action=moderate;area=reportedmembers',
5155
						'show' => allowedTo('moderate_forum'),
5156
						'is_last' => true,
5157
					)
5158
				),
5159
			),
5160
			'calendar' => array(
5161
				'title' => $txt['calendar'],
5162
				'href' => $scripturl . '?action=calendar',
5163
				'show' => $context['allow_calendar'],
5164
				'sub_buttons' => array(
5165
					'view' => array(
5166
						'title' => $txt['calendar_menu'],
5167
						'href' => $scripturl . '?action=calendar',
5168
						'show' => $context['allow_calendar_event'],
5169
					),
5170
					'post' => array(
5171
						'title' => $txt['calendar_post_event'],
5172
						'href' => $scripturl . '?action=calendar;sa=post',
5173
						'show' => $context['allow_calendar_event'],
5174
						'is_last' => true,
5175
					),
5176
				),
5177
			),
5178
			'mlist' => array(
5179
				'title' => $txt['members_title'],
5180
				'href' => $scripturl . '?action=mlist',
5181
				'show' => $context['allow_memberlist'],
5182
				'sub_buttons' => array(
5183
					'mlist_view' => array(
5184
						'title' => $txt['mlist_menu_view'],
5185
						'href' => $scripturl . '?action=mlist',
5186
						'show' => true,
5187
					),
5188
					'mlist_search' => array(
5189
						'title' => $txt['mlist_search'],
5190
						'href' => $scripturl . '?action=mlist;sa=search',
5191
						'show' => true,
5192
						'is_last' => true,
5193
					),
5194
				),
5195
				'is_last' => !$context['right_to_left'] && (!$user_info['is_guest'] || !$context['can_register']),
5196
			),
5197
			'signup' => array(
5198
				'title' => $txt['register'],
5199
				'href' => $scripturl . '?action=signup',
5200
				'show' => $user_info['is_guest'] && $context['can_register'],
5201
				'sub_buttons' => array(
5202
				),
5203
				'is_last' => !$context['right_to_left'],
5204
			),
5205
		);
5206
5207
		// Allow editing menu buttons easily.
5208
		call_integration_hook('integrate_menu_buttons', array(&$buttons));
5209
5210
		// Now we put the buttons in the context so the theme can use them.
5211
		$menu_buttons = array();
5212
		foreach ($buttons as $act => $button)
5213
			if (!empty($button['show']))
5214
			{
5215
				$button['active_button'] = false;
5216
5217
				// Make sure the last button truly is the last button.
5218
				if (!empty($button['is_last']))
5219
				{
5220
					if (isset($last_button))
5221
						unset($menu_buttons[$last_button]['is_last']);
5222
					$last_button = $act;
5223
				}
5224
5225
				// Go through the sub buttons if there are any.
5226
				if (!empty($button['sub_buttons']))
5227
					foreach ($button['sub_buttons'] as $key => $subbutton)
5228
					{
5229
						if (empty($subbutton['show']))
5230
							unset($button['sub_buttons'][$key]);
5231
5232
						// 2nd level sub buttons next...
5233
						if (!empty($subbutton['sub_buttons']))
5234
						{
5235
							foreach ($subbutton['sub_buttons'] as $key2 => $sub_button2)
5236
							{
5237
								if (empty($sub_button2['show']))
5238
									unset($button['sub_buttons'][$key]['sub_buttons'][$key2]);
5239
							}
5240
						}
5241
					}
5242
5243
				// Does this button have its own icon?
5244
				if (isset($button['icon']) && file_exists($settings['theme_dir'] . '/images/' . $button['icon']))
5245
					$button['icon'] = '<img src="' . $settings['images_url'] . '/' . $button['icon'] . '" alt="">';
5246
				elseif (isset($button['icon']) && file_exists($settings['default_theme_dir'] . '/images/' . $button['icon']))
5247
					$button['icon'] = '<img src="' . $settings['default_images_url'] . '/' . $button['icon'] . '" alt="">';
5248
				elseif (isset($button['icon']))
5249
					$button['icon'] = '<span class="main_icons ' . $button['icon'] . '"></span>';
5250
				else
5251
					$button['icon'] = '<span class="main_icons ' . $act . '"></span>';
5252
5253
				$menu_buttons[$act] = $button;
5254
			}
5255
5256
		if (!empty($cache_enable) && $cache_enable >= 2)
5257
			cache_put_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $menu_buttons, $cacheTime);
5258
	}
5259
5260
	$context['menu_buttons'] = $menu_buttons;
5261
5262
	// Logging out requires the session id in the url.
5263
	if (isset($context['menu_buttons']['logout']))
5264
		$context['menu_buttons']['logout']['href'] = sprintf($context['menu_buttons']['logout']['href'], $context['session_var'], $context['session_id']);
5265
5266
	// Figure out which action we are doing so we can set the active tab.
5267
	// Default to home.
5268
	$current_action = 'home';
5269
5270
	if (isset($context['menu_buttons'][$context['current_action']]))
5271
		$current_action = $context['current_action'];
5272
	elseif ($context['current_action'] == 'search2')
5273
		$current_action = 'search';
5274
	elseif ($context['current_action'] == 'theme')
5275
		$current_action = isset($_REQUEST['sa']) && $_REQUEST['sa'] == 'pick' ? 'profile' : 'admin';
5276
	elseif ($context['current_action'] == 'register2')
5277
		$current_action = 'register';
5278
	elseif ($context['current_action'] == 'login2' || ($user_info['is_guest'] && $context['current_action'] == 'reminder'))
5279
		$current_action = 'login';
5280
	elseif ($context['current_action'] == 'groups' && $context['allow_moderation_center'])
5281
		$current_action = 'moderate';
5282
5283
	// There are certain exceptions to the above where we don't want anything on the menu highlighted.
5284
	if ($context['current_action'] == 'profile' && !empty($context['user']['is_owner']))
5285
	{
5286
		$current_action = !empty($_GET['area']) && $_GET['area'] == 'showalerts' ? 'self_alerts' : 'self_profile';
5287
		$context[$current_action] = true;
5288
	}
5289
	elseif ($context['current_action'] == 'pm')
5290
	{
5291
		$current_action = 'self_pm';
5292
		$context['self_pm'] = true;
5293
	}
5294
5295
	$context['total_mod_reports'] = 0;
5296
	$context['total_admin_reports'] = 0;
5297
5298
	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']))
5299
	{
5300
		$context['total_mod_reports'] = $context['open_mod_reports'];
5301
		$context['menu_buttons']['moderate']['sub_buttons']['reports']['amt'] = $context['open_mod_reports'];
5302
	}
5303
5304
	// Show how many errors there are
5305
	if (!empty($context['menu_buttons']['admin']['sub_buttons']['errorlog']))
5306
	{
5307
		// Get an error count, if necessary
5308
		if (!isset($context['num_errors']))
5309
		{
5310
			$query = $smcFunc['db_query']('', '
5311
				SELECT COUNT(*)
5312
				FROM {db_prefix}log_errors',
5313
				array()
5314
			);
5315
5316
			list($context['num_errors']) = $smcFunc['db_fetch_row']($query);
5317
			$smcFunc['db_free_result']($query);
5318
		}
5319
5320
		if (!empty($context['num_errors']))
5321
		{
5322
			$context['total_admin_reports'] += $context['num_errors'];
5323
			$context['menu_buttons']['admin']['sub_buttons']['errorlog']['amt'] = $context['num_errors'];
5324
		}
5325
	}
5326
5327
	// Show number of reported members
5328
	if (!empty($context['open_member_reports']) && !empty($context['menu_buttons']['moderate']['sub_buttons']['reported_members']))
5329
	{
5330
		$context['total_mod_reports'] += $context['open_member_reports'];
5331
		$context['menu_buttons']['moderate']['sub_buttons']['reported_members']['amt'] = $context['open_member_reports'];
5332
	}
5333
5334
	if (!empty($context['unapproved_members']) && !empty($context['menu_buttons']['admin']))
5335
	{
5336
		$context['menu_buttons']['admin']['sub_buttons']['memberapprove']['amt'] = $context['unapproved_members'];
5337
		$context['total_admin_reports'] += $context['unapproved_members'];
5338
	}
5339
5340
	if ($context['total_admin_reports'] > 0 && !empty($context['menu_buttons']['admin']))
5341
	{
5342
		$context['menu_buttons']['admin']['amt'] = $context['total_admin_reports'];
5343
	}
5344
5345
	// Do we have any open reports?
5346
	if ($context['total_mod_reports'] > 0 && !empty($context['menu_buttons']['moderate']))
5347
	{
5348
		$context['menu_buttons']['moderate']['amt'] = $context['total_mod_reports'];
5349
	}
5350
5351
	// Not all actions are simple.
5352
	call_integration_hook('integrate_current_action', array(&$current_action));
5353
5354
	if (isset($context['menu_buttons'][$current_action]))
5355
		$context['menu_buttons'][$current_action]['active_button'] = true;
5356
}
5357
5358
/**
5359
 * Generate a random seed and ensure it's stored in settings.
5360
 */
5361
function smf_seed_generator()
5362
{
5363
	updateSettings(array('rand_seed' => microtime(true)));
5364
}
5365
5366
/**
5367
 * Process functions of an integration hook.
5368
 * calls all functions of the given hook.
5369
 * supports static class method calls.
5370
 *
5371
 * @param string $hook The hook name
5372
 * @param array $parameters An array of parameters this hook implements
5373
 * @return array The results of the functions
5374
 */
5375
function call_integration_hook($hook, $parameters = array())
5376
{
5377
	global $modSettings, $settings, $boarddir, $sourcedir, $db_show_debug;
5378
	global $context, $txt;
5379
5380
	if ($db_show_debug === true)
5381
		$context['debug']['hooks'][] = $hook;
5382
5383
	// Need to have some control.
5384
	if (!isset($context['instances']))
5385
		$context['instances'] = array();
5386
5387
	$results = array();
5388
	if (empty($modSettings[$hook]))
5389
		return $results;
5390
5391
	$functions = explode(',', $modSettings[$hook]);
5392
	// Loop through each function.
5393
	foreach ($functions as $function)
5394
	{
5395
		// Hook has been marked as "disabled". Skip it!
5396
		if (strpos($function, '!') !== false)
5397
			continue;
5398
5399
		$call = call_helper($function, true);
5400
5401
		// Is it valid?
5402
		if (!empty($call))
5403
			$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

5403
			$results[$function] = call_user_func_array(/** @scrutinizer ignore-type */ $call, $parameters);
Loading history...
5404
		// This failed, but we want to do so silently.
5405
		elseif (!empty($function) && !empty($context['ignore_hook_errors']))
5406
			return $results;
5407
		// Whatever it was suppose to call, it failed :(
5408
		elseif (!empty($function))
5409
		{
5410
			loadLanguage('Errors');
5411
5412
			// Get a full path to show on error.
5413
			if (strpos($function, '|') !== false)
5414
			{
5415
				list ($file, $string) = explode('|', $function);
5416
				$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'])));
5417
				log_error(sprintf($txt['hook_fail_call_to'], $string, $absPath), 'general');
5418
			}
5419
			// "Assume" the file resides on $boarddir somewhere...
5420
			else
5421
				log_error(sprintf($txt['hook_fail_call_to'], $function, $boarddir), 'general');
5422
		}
5423
	}
5424
5425
	return $results;
5426
}
5427
5428
/**
5429
 * Add a function for integration hook.
5430
 * does nothing if the function is already added.
5431
 *
5432
 * @param string $hook The complete hook name.
5433
 * @param string $function The function name. Can be a call to a method via Class::method.
5434
 * @param bool $permanent If true, updates the value in settings table.
5435
 * @param string $file The file. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
5436
 * @param bool $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
5437
 */
5438
function add_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
5439
{
5440
	global $smcFunc, $modSettings;
5441
5442
	// Any objects?
5443
	if ($object)
5444
		$function = $function . '#';
5445
5446
	// Any files  to load?
5447
	if (!empty($file) && is_string($file))
5448
		$function = $file . (!empty($function) ? '|' . $function : '');
5449
5450
	// Get the correct string.
5451
	$integration_call = $function;
5452
5453
	// Is it going to be permanent?
5454
	if ($permanent)
5455
	{
5456
		$request = $smcFunc['db_query']('', '
5457
			SELECT value
5458
			FROM {db_prefix}settings
5459
			WHERE variable = {string:variable}',
5460
			array(
5461
				'variable' => $hook,
5462
			)
5463
		);
5464
		list ($current_functions) = $smcFunc['db_fetch_row']($request);
5465
		$smcFunc['db_free_result']($request);
5466
5467
		if (!empty($current_functions))
5468
		{
5469
			$current_functions = explode(',', $current_functions);
5470
			if (in_array($integration_call, $current_functions))
5471
				return;
5472
5473
			$permanent_functions = array_merge($current_functions, array($integration_call));
5474
		}
5475
		else
5476
			$permanent_functions = array($integration_call);
5477
5478
		updateSettings(array($hook => implode(',', $permanent_functions)));
5479
	}
5480
5481
	// Make current function list usable.
5482
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
5483
5484
	// Do nothing, if it's already there.
5485
	if (in_array($integration_call, $functions))
5486
		return;
5487
5488
	$functions[] = $integration_call;
5489
	$modSettings[$hook] = implode(',', $functions);
5490
}
5491
5492
/**
5493
 * Remove an integration hook function.
5494
 * Removes the given function from the given hook.
5495
 * Does nothing if the function is not available.
5496
 *
5497
 * @param string $hook The complete hook name.
5498
 * @param string $function The function name. Can be a call to a method via Class::method.
5499
 * @param boolean $permanent Irrelevant for the function itself but need to declare it to match
5500
 * @param string $file The filename. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
5501
 * @param boolean $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
5502
 * @see add_integration_function
5503
 */
5504
function remove_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
5505
{
5506
	global $smcFunc, $modSettings;
5507
5508
	// Any objects?
5509
	if ($object)
5510
		$function = $function . '#';
5511
5512
	// Any files  to load?
5513
	if (!empty($file) && is_string($file))
5514
		$function = $file . '|' . $function;
5515
5516
	// Get the correct string.
5517
	$integration_call = $function;
5518
5519
	// Get the permanent functions.
5520
	$request = $smcFunc['db_query']('', '
5521
		SELECT value
5522
		FROM {db_prefix}settings
5523
		WHERE variable = {string:variable}',
5524
		array(
5525
			'variable' => $hook,
5526
		)
5527
	);
5528
	list ($current_functions) = $smcFunc['db_fetch_row']($request);
5529
	$smcFunc['db_free_result']($request);
5530
5531
	if (!empty($current_functions))
5532
	{
5533
		$current_functions = explode(',', $current_functions);
5534
5535
		if (in_array($integration_call, $current_functions))
5536
			updateSettings(array($hook => implode(',', array_diff($current_functions, array($integration_call)))));
5537
	}
5538
5539
	// Turn the function list into something usable.
5540
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
5541
5542
	// You can only remove it if it's available.
5543
	if (!in_array($integration_call, $functions))
5544
		return;
5545
5546
	$functions = array_diff($functions, array($integration_call));
5547
	$modSettings[$hook] = implode(',', $functions);
5548
}
5549
5550
/**
5551
 * Receives a string and tries to figure it out if its a method or a function.
5552
 * If a method is found, it looks for a "#" which indicates SMF should create a new instance of the given class.
5553
 * Checks the string/array for is_callable() and return false/fatal_lang_error is the given value results in a non callable string/array.
5554
 * Prepare and returns a callable depending on the type of method/function found.
5555
 *
5556
 * @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)
5557
 * @param boolean $return If true, the function will not call the function/method but instead will return the formatted string.
5558
 * @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.
5559
 */
5560
function call_helper($string, $return = false)
5561
{
5562
	global $context, $smcFunc, $txt, $db_show_debug;
5563
5564
	// Really?
5565
	if (empty($string))
5566
		return false;
5567
5568
	// An array? should be a "callable" array IE array(object/class, valid_callable).
5569
	// A closure? should be a callable one.
5570
	if (is_array($string) || $string instanceof Closure)
5571
		return $return ? $string : (is_callable($string) ? call_user_func($string) : false);
5572
5573
	// No full objects, sorry! pass a method or a property instead!
5574
	if (is_object($string))
5575
		return false;
5576
5577
	// Stay vitaminized my friends...
5578
	$string = $smcFunc['htmlspecialchars']($smcFunc['htmltrim']($string));
5579
5580
	// Is there a file to load?
5581
	$string = load_file($string);
5582
5583
	// Loaded file failed
5584
	if (empty($string))
5585
		return false;
5586
5587
	// Found a method.
5588
	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

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

5590
		list ($class, $method) = explode('::', /** @scrutinizer ignore-type */ $string);
Loading history...
5591
5592
		// Check if a new object will be created.
5593
		if (strpos($method, '#') !== false)
5594
		{
5595
			// Need to remove the # thing.
5596
			$method = str_replace('#', '', $method);
5597
5598
			// Don't need to create a new instance for every method.
5599
			if (empty($context['instances'][$class]) || !($context['instances'][$class] instanceof $class))
5600
			{
5601
				$context['instances'][$class] = new $class;
5602
5603
				// Add another one to the list.
5604
				if ($db_show_debug === true)
5605
				{
5606
					if (!isset($context['debug']['instances']))
5607
						$context['debug']['instances'] = array();
5608
5609
					$context['debug']['instances'][$class] = $class;
5610
				}
5611
			}
5612
5613
			$func = array($context['instances'][$class], $method);
5614
		}
5615
5616
		// Right then. This is a call to a static method.
5617
		else
5618
			$func = array($class, $method);
5619
	}
5620
5621
	// Nope! just a plain regular function.
5622
	else
5623
		$func = $string;
5624
5625
	// We can't call this helper, but we want to silently ignore this.
5626
	if (!is_callable($func, false, $callable_name) && !empty($context['ignore_hook_errors']))
5627
		return false;
5628
5629
	// Right, we got what we need, time to do some checks.
5630
	elseif (!is_callable($func, false, $callable_name))
5631
	{
5632
		loadLanguage('Errors');
5633
		log_error(sprintf($txt['sub_action_fail'], $callable_name), 'general');
5634
5635
		// Gotta tell everybody.
5636
		return false;
5637
	}
5638
5639
	// Everything went better than expected.
5640
	else
5641
	{
5642
		// What are we gonna do about it?
5643
		if ($return)
5644
			return $func;
5645
5646
		// If this is a plain function, avoid the heat of calling call_user_func().
5647
		else
5648
		{
5649
			if (is_array($func))
5650
				call_user_func($func);
5651
5652
			else
5653
				$func();
5654
		}
5655
	}
5656
}
5657
5658
/**
5659
 * Receives a string and tries to figure it out if it contains info to load a file.
5660
 * Checks for a | (pipe) symbol and tries to load a file with the info given.
5661
 * 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.
5662
 *
5663
 * @param string $string The string containing a valid format.
5664
 * @return string|boolean The given string with the pipe and file info removed. Boolean false if the file couldn't be loaded.
5665
 */
5666
function load_file($string)
5667
{
5668
	global $sourcedir, $txt, $boarddir, $settings, $context;
5669
5670
	if (empty($string))
5671
		return false;
5672
5673
	if (strpos($string, '|') !== false)
5674
	{
5675
		list ($file, $string) = explode('|', $string);
5676
5677
		// Match the wildcards to their regular vars.
5678
		if (empty($settings['theme_dir']))
5679
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
5680
5681
		else
5682
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
5683
5684
		// Load the file if it can be loaded.
5685
		if (file_exists($absPath))
5686
			require_once($absPath);
5687
5688
		// No? try a fallback to $sourcedir
5689
		else
5690
		{
5691
			$absPath = $sourcedir . '/' . $file;
5692
5693
			if (file_exists($absPath))
5694
				require_once($absPath);
5695
5696
			// Sorry, can't do much for you at this point.
5697
			elseif (empty($context['uninstalling']))
5698
			{
5699
				loadLanguage('Errors');
5700
				log_error(sprintf($txt['hook_fail_loading_file'], $absPath), 'general');
5701
5702
				// File couldn't be loaded.
5703
				return false;
5704
			}
5705
		}
5706
	}
5707
5708
	return $string;
5709
}
5710
5711
/**
5712
 * Get the contents of a URL, irrespective of allow_url_fopen.
5713
 *
5714
 * - reads the contents of an http or ftp address and returns the page in a string
5715
 * - will accept up to 3 page redirections (redirectio_level in the function call is private)
5716
 * - if post_data is supplied, the value and length is posted to the given url as form data
5717
 * - URL must be supplied in lowercase
5718
 *
5719
 * @param string $url The URL
5720
 * @param string $post_data The data to post to the given URL
5721
 * @param bool $keep_alive Whether to send keepalive info
5722
 * @param int $redirection_level How many levels of redirection
5723
 * @return string|false The fetched data or false on failure
5724
 */
5725
function fetch_web_data($url, $post_data = '', $keep_alive = false, $redirection_level = 0)
5726
{
5727
	global $webmaster_email, $sourcedir, $txt;
5728
	static $keep_alive_dom = null, $keep_alive_fp = null;
5729
5730
	preg_match('~^(http|ftp)(s)?://([^/:]+)(:(\d+))?(.+)$~', $url, $match);
5731
5732
	// No scheme? No data for you!
5733
	if (empty($match[1]))
5734
		return false;
5735
5736
	// An FTP url. We should try connecting and RETRieving it...
5737
	elseif ($match[1] == 'ftp')
5738
	{
5739
		// Include the file containing the ftp_connection class.
5740
		require_once($sourcedir . '/Class-Package.php');
5741
5742
		// Establish a connection and attempt to enable passive mode.
5743
		$ftp = new ftp_connection(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? 21 : $match[5], 'anonymous', $webmaster_email);
5744
		if ($ftp->error !== false || !$ftp->passive())
0 ignored issues
show
introduced by
The condition $ftp->error !== false is always true.
Loading history...
5745
			return false;
5746
5747
		// I want that one *points*!
5748
		fwrite($ftp->connection, 'RETR ' . $match[6] . "\r\n");
5749
5750
		// Since passive mode worked (or we would have returned already!) open the connection.
5751
		$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...
5752
		if (!$fp)
5753
			return false;
5754
5755
		// The server should now say something in acknowledgement.
5756
		$ftp->check_response(150);
5757
5758
		$data = '';
5759
		while (!feof($fp))
5760
			$data .= fread($fp, 4096);
5761
		fclose($fp);
5762
5763
		// All done, right?  Good.
5764
		$ftp->check_response(226);
5765
		$ftp->close();
5766
	}
5767
5768
	// This is more likely; a standard HTTP URL.
5769
	elseif (isset($match[1]) && $match[1] == 'http')
5770
	{
5771
		// First try to use fsockopen, because it is fastest.
5772
		if ($keep_alive && $match[3] == $keep_alive_dom)
5773
			$fp = $keep_alive_fp;
5774
		if (empty($fp))
5775
		{
5776
			// Open the socket on the port we want...
5777
			$fp = @fsockopen(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? ($match[2] ? 443 : 80) : $match[5], $err, $err, 5);
5778
		}
5779
		if (!empty($fp))
5780
		{
5781
			if ($keep_alive)
5782
			{
5783
				$keep_alive_dom = $match[3];
5784
				$keep_alive_fp = $fp;
5785
			}
5786
5787
			// I want this, from there, and I'm not going to be bothering you for more (probably.)
5788
			if (empty($post_data))
5789
			{
5790
				fwrite($fp, 'GET ' . ($match[6] !== '/' ? str_replace(' ', '%20', $match[6]) : '') . ' HTTP/1.0' . "\r\n");
5791
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
5792
				fwrite($fp, 'user-agent: '. SMF_USER_AGENT . "\r\n");
5793
				if ($keep_alive)
5794
					fwrite($fp, 'connection: Keep-Alive' . "\r\n\r\n");
5795
				else
5796
					fwrite($fp, 'connection: close' . "\r\n\r\n");
5797
			}
5798
			else
5799
			{
5800
				fwrite($fp, 'POST ' . ($match[6] !== '/' ? $match[6] : '') . ' HTTP/1.0' . "\r\n");
5801
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
5802
				fwrite($fp, 'user-agent: '. SMF_USER_AGENT . "\r\n");
5803
				if ($keep_alive)
5804
					fwrite($fp, 'connection: Keep-Alive' . "\r\n");
5805
				else
5806
					fwrite($fp, 'connection: close' . "\r\n");
5807
				fwrite($fp, 'content-type: application/x-www-form-urlencoded' . "\r\n");
5808
				fwrite($fp, 'content-length: ' . strlen($post_data) . "\r\n\r\n");
5809
				fwrite($fp, $post_data);
5810
			}
5811
5812
			$response = fgets($fp, 768);
5813
5814
			// Redirect in case this location is permanently or temporarily moved.
5815
			if ($redirection_level < 3 && preg_match('~^HTTP/\S+\s+30[127]~i', $response) === 1)
5816
			{
5817
				$header = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $header is dead and can be removed.
Loading history...
5818
				$location = '';
5819
				while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
5820
					if (stripos($header, 'location:') !== false)
5821
						$location = trim(substr($header, strpos($header, ':') + 1));
5822
5823
				if (empty($location))
5824
					return false;
5825
				else
5826
				{
5827
					if (!$keep_alive)
5828
						fclose($fp);
5829
					return fetch_web_data($location, $post_data, $keep_alive, $redirection_level + 1);
5830
				}
5831
			}
5832
5833
			// Make sure we get a 200 OK.
5834
			elseif (preg_match('~^HTTP/\S+\s+20[01]~i', $response) === 0)
5835
				return false;
5836
5837
			// Skip the headers...
5838
			while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
5839
			{
5840
				if (preg_match('~content-length:\s*(\d+)~i', $header, $match) != 0)
5841
					$content_length = $match[1];
5842
				elseif (preg_match('~connection:\s*close~i', $header) != 0)
5843
				{
5844
					$keep_alive_dom = null;
5845
					$keep_alive = false;
5846
				}
5847
5848
				continue;
5849
			}
5850
5851
			$data = '';
5852
			if (isset($content_length))
5853
			{
5854
				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...
5855
					$data .= fread($fp, $content_length - strlen($data));
5856
			}
5857
			else
5858
			{
5859
				while (!feof($fp))
5860
					$data .= fread($fp, 4096);
5861
			}
5862
5863
			if (!$keep_alive)
5864
				fclose($fp);
5865
		}
5866
5867
		// If using fsockopen didn't work, try to use cURL if available.
5868
		elseif (function_exists('curl_init'))
5869
		{
5870
			// Include the file containing the curl_fetch_web_data class.
5871
			require_once($sourcedir . '/Class-CurlFetchWeb.php');
5872
5873
			$fetch_data = new curl_fetch_web_data();
5874
			$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

5874
			$fetch_data->get_url_data($url, /** @scrutinizer ignore-type */ $post_data);
Loading history...
5875
5876
			// no errors and a 200 result, then we have a good dataset, well we at least have data. ;)
5877
			if ($fetch_data->result('code') == 200 && !$fetch_data->result('error'))
5878
				$data = $fetch_data->result('body');
5879
			else
5880
				return false;
5881
		}
5882
5883
		// Neither fsockopen nor curl are available. Well, phooey.
5884
		else
5885
			return false;
5886
	}
5887
	else
5888
	{
5889
		// Umm, this shouldn't happen?
5890
		loadLanguage('Errors');
5891
		trigger_error($txt['fetch_web_data_bad_url'], E_USER_NOTICE);
5892
		$data = false;
5893
	}
5894
5895
	return $data;
5896
}
5897
5898
/**
5899
 * Attempts to determine the MIME type of some data or a file.
5900
 *
5901
 * @param string $data The data to check, or the path or URL of a file to check.
5902
 * @param string $is_path If true, $data is a path or URL to a file.
5903
 * @return string|bool A MIME type, or false if we cannot determine it.
5904
 */
5905
function get_mime_type($data, $is_path = false)
5906
{
5907
	global $cachedir;
5908
5909
	$finfo_loaded = extension_loaded('fileinfo');
5910
	$exif_loaded = extension_loaded('exif') && function_exists('image_type_to_mime_type');
5911
5912
	// Oh well. We tried.
5913
	if (!$finfo_loaded && !$exif_loaded)
5914
		return false;
5915
5916
	// Start with the 'empty' MIME type.
5917
	$mime_type = 'application/x-empty';
5918
5919
	if ($finfo_loaded)
5920
	{
5921
		// Just some nice, simple data to analyze.
5922
		if (empty($is_path))
5923
			$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
5924
5925
		// A file, or maybe a URL?
5926
		else
5927
		{
5928
			// Local file.
5929
			if (file_exists($data))
5930
				$mime_type = mime_content_type($data);
5931
5932
			// URL.
5933
			elseif ($data = fetch_web_data($data))
5934
				$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
5935
		}
5936
	}
5937
	// Workaround using Exif requires a local file.
5938
	else
5939
	{
5940
		// If $data is a URL to fetch, do so.
5941
		if (!empty($is_path) && !file_exists($data) && url_exists($data))
5942
		{
5943
			$data = fetch_web_data($data);
5944
			$is_path = false;
5945
		}
5946
5947
		// If we don't have a local file, create one and use it.
5948
		if (empty($is_path))
5949
		{
5950
			$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

5950
			$temp_file = tempnam($cachedir, md5(/** @scrutinizer ignore-type */ $data));
Loading history...
5951
			file_put_contents($temp_file, $data);
5952
			$is_path = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $is_path is dead and can be removed.
Loading history...
5953
			$data = $temp_file;
5954
		}
5955
5956
		$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

5956
		$imagetype = @exif_imagetype(/** @scrutinizer ignore-type */ $data);
Loading history...
5957
5958
		if (isset($temp_file))
5959
			unlink($temp_file);
5960
5961
		// Unfortunately, this workaround only works for image files.
5962
		if ($imagetype !== false)
5963
			$mime_type = image_type_to_mime_type($imagetype);
5964
	}
5965
5966
	return $mime_type;
5967
}
5968
5969
/**
5970
 * Checks whether a file or data has the expected MIME type.
5971
 *
5972
 * @param string $data The data to check, or the path or URL of a file to check.
5973
 * @param string $type_pattern A regex pattern to match the acceptable MIME types.
5974
 * @param string $is_path If true, $data is a path or URL to a file.
5975
 * @return int 1 if the detected MIME type matches the pattern, 0 if it doesn't, or 2 if we can't check.
5976
 */
5977
function check_mime_type($data, $type_pattern, $is_path = false)
5978
{
5979
	// Get the MIME type.
5980
	$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

5980
	$mime_type = get_mime_type($data, /** @scrutinizer ignore-type */ $is_path);
Loading history...
5981
5982
	// Couldn't determine it.
5983
	if ($mime_type === false)
5984
		return 2;
5985
5986
	// Check whether the MIME type matches expectations.
5987
	return (int) @preg_match('~' . $type_pattern . '~', $mime_type);
5988
}
5989
5990
/**
5991
 * Prepares an array of "likes" info for the topic specified by $topic
5992
 *
5993
 * @param integer $topic The topic ID to fetch the info from.
5994
 * @return array An array of IDs of messages in the specified topic that the current user likes
5995
 */
5996
function prepareLikesContext($topic)
5997
{
5998
	global $user_info, $smcFunc;
5999
6000
	// Make sure we have something to work with.
6001
	if (empty($topic))
6002
		return array();
6003
6004
	// We already know the number of likes per message, we just want to know whether the current user liked it or not.
6005
	$user = $user_info['id'];
6006
	$cache_key = 'likes_topic_' . $topic . '_' . $user;
6007
	$ttl = 180;
6008
6009
	if (($temp = cache_get_data($cache_key, $ttl)) === null)
6010
	{
6011
		$temp = array();
6012
		$request = $smcFunc['db_query']('', '
6013
			SELECT content_id
6014
			FROM {db_prefix}user_likes AS l
6015
				INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg)
6016
			WHERE l.id_member = {int:current_user}
6017
				AND l.content_type = {literal:msg}
6018
				AND m.id_topic = {int:topic}',
6019
			array(
6020
				'current_user' => $user,
6021
				'topic' => $topic,
6022
			)
6023
		);
6024
		while ($row = $smcFunc['db_fetch_assoc']($request))
6025
			$temp[] = (int) $row['content_id'];
6026
6027
		cache_put_data($cache_key, $temp, $ttl);
6028
	}
6029
6030
	return $temp;
6031
}
6032
6033
/**
6034
 * Microsoft uses their own character set Code Page 1252 (CP1252), which is a
6035
 * superset of ISO 8859-1, defining several characters between DEC 128 and 159
6036
 * that are not normally displayable.  This converts the popular ones that
6037
 * appear from a cut and paste from windows.
6038
 *
6039
 * @param string $string The string
6040
 * @return string The sanitized string
6041
 */
6042
function sanitizeMSCutPaste($string)
6043
{
6044
	global $context;
6045
6046
	if (empty($string))
6047
		return $string;
6048
6049
	// UTF-8 occurences of MS special characters
6050
	$findchars_utf8 = array(
6051
		"\xe2\x80\x9a",	// single low-9 quotation mark
6052
		"\xe2\x80\x9e",	// double low-9 quotation mark
6053
		"\xe2\x80\xa6",	// horizontal ellipsis
6054
		"\xe2\x80\x98",	// left single curly quote
6055
		"\xe2\x80\x99",	// right single curly quote
6056
		"\xe2\x80\x9c",	// left double curly quote
6057
		"\xe2\x80\x9d",	// right double curly quote
6058
	);
6059
6060
	// windows 1252 / iso equivalents
6061
	$findchars_iso = array(
6062
		chr(130),
6063
		chr(132),
6064
		chr(133),
6065
		chr(145),
6066
		chr(146),
6067
		chr(147),
6068
		chr(148),
6069
	);
6070
6071
	// safe replacements
6072
	$replacechars = array(
6073
		',',	// &sbquo;
6074
		',,',	// &bdquo;
6075
		'...',	// &hellip;
6076
		"'",	// &lsquo;
6077
		"'",	// &rsquo;
6078
		'"',	// &ldquo;
6079
		'"',	// &rdquo;
6080
	);
6081
6082
	if ($context['utf8'])
6083
		$string = str_replace($findchars_utf8, $replacechars, $string);
6084
	else
6085
		$string = str_replace($findchars_iso, $replacechars, $string);
6086
6087
	return $string;
6088
}
6089
6090
/**
6091
 * Decode numeric html entities to their ascii or UTF8 equivalent character.
6092
 *
6093
 * Callback function for preg_replace_callback in subs-members
6094
 * Uses capture group 2 in the supplied array
6095
 * Does basic scan to ensure characters are inside a valid range
6096
 *
6097
 * @param array $matches An array of matches (relevant info should be the 3rd item)
6098
 * @return string A fixed string
6099
 */
6100
function replaceEntities__callback($matches)
6101
{
6102
	global $context;
6103
6104
	if (!isset($matches[2]))
6105
		return '';
6106
6107
	$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

6107
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
6108
6109
	// remove left to right / right to left overrides
6110
	if ($num === 0x202D || $num === 0x202E)
6111
		return '';
6112
6113
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
6114
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
6115
		return '&#' . $num . ';';
6116
6117
	if (empty($context['utf8']))
6118
	{
6119
		// no control characters
6120
		if ($num < 0x20)
6121
			return '';
6122
		// text is text
6123
		elseif ($num < 0x80)
6124
			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

6124
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
6125
		// all others get html-ised
6126
		else
6127
			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

6127
			return '&#' . /** @scrutinizer ignore-type */ $matches[2] . ';';
Loading history...
6128
	}
6129
	else
6130
	{
6131
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
6132
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
6133
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
6134
			return '';
6135
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
6136
		elseif ($num < 0x80)
6137
			return chr($num);
6138
		// <0x800 (2048)
6139
		elseif ($num < 0x800)
6140
			return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
6141
		// < 0x10000 (65536)
6142
		elseif ($num < 0x10000)
6143
			return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6144
		// <= 0x10FFFF (1114111)
6145
		else
6146
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6147
	}
6148
}
6149
6150
/**
6151
 * Converts html entities to utf8 equivalents
6152
 *
6153
 * Callback function for preg_replace_callback
6154
 * Uses capture group 1 in the supplied array
6155
 * Does basic checks to keep characters inside a viewable range.
6156
 *
6157
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
6158
 * @return string The fixed string
6159
 */
6160
function fixchar__callback($matches)
6161
{
6162
	if (!isset($matches[1]))
6163
		return '';
6164
6165
	$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

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

6173
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
6174
	// <0x800 (2048)
6175
	elseif ($num < 0x800)
6176
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
6177
	// < 0x10000 (65536)
6178
	elseif ($num < 0x10000)
6179
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6180
	// <= 0x10FFFF (1114111)
6181
	else
6182
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6183
}
6184
6185
/**
6186
 * Strips out invalid html entities, replaces others with html style &#123; codes
6187
 *
6188
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
6189
 * strpos, strlen, substr etc
6190
 *
6191
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
6192
 * @return string The fixed string
6193
 */
6194
function entity_fix__callback($matches)
6195
{
6196
	if (!isset($matches[2]))
6197
		return '';
6198
6199
	$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

6199
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
6200
6201
	// we don't allow control characters, characters out of range, byte markers, etc
6202
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
6203
		return '';
6204
	else
6205
		return '&#' . $num . ';';
6206
}
6207
6208
/**
6209
 * Return a Gravatar URL based on
6210
 * - the supplied email address,
6211
 * - the global maximum rating,
6212
 * - the global default fallback,
6213
 * - maximum sizes as set in the admin panel.
6214
 *
6215
 * It is SSL aware, and caches most of the parameters.
6216
 *
6217
 * @param string $email_address The user's email address
6218
 * @return string The gravatar URL
6219
 */
6220
function get_gravatar_url($email_address)
6221
{
6222
	global $modSettings, $smcFunc;
6223
	static $url_params = null;
6224
6225
	if ($url_params === null)
6226
	{
6227
		$ratings = array('G', 'PG', 'R', 'X');
6228
		$defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank');
6229
		$url_params = array();
6230
		if (!empty($modSettings['gravatarMaxRating']) && in_array($modSettings['gravatarMaxRating'], $ratings))
6231
			$url_params[] = 'rating=' . $modSettings['gravatarMaxRating'];
6232
		if (!empty($modSettings['gravatarDefault']) && in_array($modSettings['gravatarDefault'], $defaults))
6233
			$url_params[] = 'default=' . $modSettings['gravatarDefault'];
6234
		if (!empty($modSettings['avatar_max_width_external']))
6235
			$size_string = (int) $modSettings['avatar_max_width_external'];
6236
		if (!empty($modSettings['avatar_max_height_external']) && !empty($size_string))
6237
			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...
6238
				$size_string = $modSettings['avatar_max_height_external'];
6239
6240
		if (!empty($size_string))
6241
			$url_params[] = 's=' . $size_string;
6242
	}
6243
	$http_method = !empty($modSettings['force_ssl']) ? 'https://secure' : 'http://www';
6244
6245
	return $http_method . '.gravatar.com/avatar/' . md5($smcFunc['strtolower']($email_address)) . '?' . implode('&', $url_params);
6246
}
6247
6248
/**
6249
 * Get a list of time zones.
6250
 *
6251
 * @param string $when The date/time for which to calculate the time zone values.
6252
 *		May be a Unix timestamp or any string that strtotime() can understand.
6253
 *		Defaults to 'now'.
6254
 * @return array An array of time zone identifiers and label text.
6255
 */
6256
function smf_list_timezones($when = 'now')
6257
{
6258
	global $modSettings, $tztxt, $txt, $context, $cur_profile, $sourcedir;
6259
	static $timezones_when = array();
6260
6261
	require_once($sourcedir . '/Subs-Timezones.php');
6262
6263
	// Parseable datetime string?
6264
	if (is_int($timestamp = strtotime($when)))
0 ignored issues
show
introduced by
The condition is_int($timestamp = strtotime($when)) is always true.
Loading history...
6265
		$when = $timestamp;
6266
6267
	// A Unix timestamp?
6268
	elseif (is_numeric($when))
6269
		$when = intval($when);
6270
6271
	// Invalid value? Just get current Unix timestamp.
6272
	else
6273
		$when = time();
6274
6275
	// No point doing this over if we already did it once
6276
	if (isset($timezones_when[$when]))
6277
		return $timezones_when[$when];
6278
6279
	// We'll need these too
6280
	$date_when = date_create('@' . $when);
6281
	$later = strtotime('@' . $when . ' + 1 year');
6282
6283
	// Load up any custom time zone descriptions we might have
6284
	loadLanguage('Timezones');
6285
6286
	$tzid_metazones = get_tzid_metazones($later);
6287
6288
	// Should we put time zones from certain countries at the top of the list?
6289
	$priority_countries = !empty($modSettings['timezone_priority_countries']) ? explode(',', $modSettings['timezone_priority_countries']) : array();
6290
6291
	$priority_tzids = array();
6292
	foreach ($priority_countries as $country)
6293
	{
6294
		$country_tzids = get_sorted_tzids_for_country($country);
6295
6296
		if (!empty($country_tzids))
6297
			$priority_tzids = array_merge($priority_tzids, $country_tzids);
6298
	}
6299
6300
	// Antarctic research stations should be listed last, unless you're running a penguin forum
6301
	$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...
6302
6303
	$normal_priority_tzids = array_diff(array_unique(array_merge(array_keys($tzid_metazones), timezone_identifiers_list())), $priority_tzids, $low_priority_tzids);
0 ignored issues
show
Bug introduced by
timezone_identifiers_list() of type void is incompatible with the type array expected by parameter $arrays of array_merge(). ( Ignorable by Annotation )

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

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

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

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

}

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

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

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

Loading history...
6304
6305
	// Process them in order of importance.
6306
	$tzids = array_merge($priority_tzids, $normal_priority_tzids, $low_priority_tzids);
6307
6308
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
6309
	$dst_types = array();
6310
	$labels = array();
6311
	$offsets = array();
6312
	foreach ($tzids as $tzid)
6313
	{
6314
		// We don't want UTC right now
6315
		if ($tzid == 'UTC')
6316
			continue;
6317
6318
		$tz = @timezone_open($tzid);
6319
6320
		if ($tz == null)
6321
			continue;
6322
6323
		// First, get the set of transition rules for this tzid
6324
		$tzinfo = timezone_transitions_get($tz, $when, $later);
6325
6326
		// Use the entire set of transition rules as the array *key* so we can avoid duplicates
6327
		$tzkey = serialize($tzinfo);
6328
6329
		// ...But make sure to include all explicitly defined meta-zones.
6330
		if (isset($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid]))
6331
			$tzkey = serialize(array_merge($tzinfo, array('metazone' => $tzid_metazones[$tzid])));
6332
6333
		// Don't overwrite our preferred tzids
6334
		if (empty($zones[$tzkey]['tzid']))
6335
		{
6336
			$zones[$tzkey]['tzid'] = $tzid;
6337
			$zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
6338
6339
			foreach ($tzinfo as $transition) {
6340
				$zones[$tzkey]['abbrs'][] = $transition['abbr'];
6341
			}
6342
6343
			if (isset($tzid_metazones[$tzid]))
6344
				$zones[$tzkey]['metazone'] = $tzid_metazones[$tzid];
6345
			else
6346
			{
6347
				$tzgeo = timezone_location_get($tz);
6348
				$country_tzids = get_sorted_tzids_for_country($tzgeo['country_code']);
6349
6350
				if (count($country_tzids) === 1)
6351
					$zones[$tzkey]['metazone'] = $txt['iso3166'][$tzgeo['country_code']];
6352
			}
6353
		}
6354
6355
		// A time zone from a prioritized country?
6356
		if (in_array($tzid, $priority_tzids))
6357
			$priority_zones[$tzkey] = true;
6358
6359
		// Keep track of the location for this tzid.
6360
		if (!empty($txt[$tzid]))
6361
			$zones[$tzkey]['locations'][] = $txt[$tzid];
6362
		else
6363
		{
6364
			$tzid_parts = explode('/', $tzid);
6365
			$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
6366
		}
6367
6368
		// Keep track of the current offset for this tzid.
6369
		$offsets[$tzkey] = $tzinfo[0]['offset'];
6370
6371
		// Keep track of the Standard Time offset for this tzid.
6372
		foreach ($tzinfo as $transition)
6373
		{
6374
			if (!$transition['isdst'])
6375
			{
6376
				$std_offsets[$tzkey] = $transition['offset'];
6377
				break;
6378
			}
6379
		}
6380
		if (!isset($std_offsets[$tzkey]))
6381
			$std_offsets[$tzkey] = $tzinfo[0]['offset'];
6382
6383
		// Figure out the "meta-zone" info for the label
6384
		if (empty($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid]))
6385
		{
6386
			$zones[$tzkey]['metazone'] = $tzid_metazones[$tzid];
6387
			$zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
6388
		}
6389
		$dst_types[$tzkey] = count($tzinfo) > 1 ? 'c' : ($tzinfo[0]['isdst'] ? 't' : 'f');
6390
		$labels[$tzkey] = !empty($zones[$tzkey]['metazone']) && !empty($tztxt[$zones[$tzkey]['metazone']]) ? $tztxt[$zones[$tzkey]['metazone']] : '';
6391
6392
		// Remember this for later
6393
		if (isset($cur_profile['timezone']) && $cur_profile['timezone'] == $tzid)
6394
			$member_tzkey = $tzkey;
6395
		if (isset($context['event']['tz']) && $context['event']['tz'] == $tzid)
6396
			$event_tzkey = $tzkey;
6397
	}
6398
6399
	// Sort by current offset, then standard offset, then DST type, then label.
6400
	array_multisort($offsets, SORT_DESC, SORT_NUMERIC, $std_offsets, SORT_DESC, SORT_NUMERIC, $dst_types, SORT_ASC, $labels, SORT_ASC, $zones);
0 ignored issues
show
Bug introduced by
SORT_DESC cannot be passed to array_multisort() as the parameter $rest expects a reference. ( Ignorable by Annotation )

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

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

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

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

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

6400
	array_multisort($offsets, SORT_DESC, /** @scrutinizer ignore-type */ SORT_NUMERIC, $std_offsets, SORT_DESC, SORT_NUMERIC, $dst_types, SORT_ASC, $labels, SORT_ASC, $zones);
Loading history...
Comprehensibility Best Practice introduced by
The variable $std_offsets does not seem to be defined for all execution paths leading up to this point.
Loading history...
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...
6401
6402
	// Build the final array of formatted values
6403
	$priority_timezones = array();
6404
	$timezones = array();
6405
	foreach ($zones as $tzkey => $tzvalue)
6406
	{
6407
		date_timezone_set($date_when, timezone_open($tzvalue['tzid']));
6408
6409
		// Use the human friendly time zone name, if there is one.
6410
		$desc = '';
6411
		if (!empty($tzvalue['metazone']))
6412
		{
6413
			if (!empty($tztxt[$tzvalue['metazone']]))
6414
				$metazone = $tztxt[$tzvalue['metazone']];
6415
			else
6416
				$metazone = sprintf($tztxt['generic_timezone'], $tzvalue['metazone'], '%1$s');
6417
6418
			switch ($tzvalue['dst_type'])
6419
			{
6420
				case 0:
6421
					$desc = sprintf($metazone, $tztxt['daylight_saving_time_false']);
6422
					break;
6423
6424
				case 1:
6425
					$desc = sprintf($metazone, '');
6426
					break;
6427
6428
				case 2:
6429
					$desc = sprintf($metazone, $tztxt['daylight_saving_time_true']);
6430
					break;
6431
			}
6432
		}
6433
		// Otherwise, use the list of locations (max 5, so things don't get silly)
6434
		else
6435
			$desc = implode(', ', array_slice(array_unique($tzvalue['locations']), 0, 5)) . (count($tzvalue['locations']) > 5 ? ', ' . $txt['etc'] : '');
6436
6437
		// We don't want abbreviations like '+03' or '-11'.
6438
		$abbrs = array_filter(
6439
			$tzvalue['abbrs'],
6440
			function ($abbr)
6441
			{
6442
				return !strspn($abbr, '+-');
6443
			}
6444
		);
6445
		$abbrs = count($abbrs) == count($tzvalue['abbrs']) ? array_unique($abbrs) : array();
6446
6447
		// Show the UTC offset and abbreviation(s).
6448
		$desc = '[UTC' . date_format($date_when, 'P') . '] - ' . str_replace('  ', ' ', $desc) . (!empty($abbrs) ? ' (' . implode('/', $abbrs) . ')' : '');
6449
6450
		if (isset($priority_zones[$tzkey]))
6451
			$priority_timezones[$tzvalue['tzid']] = $desc;
6452
		else
6453
			$timezones[$tzvalue['tzid']] = $desc;
6454
6455
		// Automatically fix orphaned time zones.
6456
		if (isset($member_tzkey) && $member_tzkey == $tzkey)
6457
			$cur_profile['timezone'] = $tzvalue['tzid'];
6458
		if (isset($event_tzkey) && $event_tzkey == $tzkey)
6459
			$context['event']['tz'] = $tzvalue['tzid'];
6460
	}
6461
6462
	if (!empty($priority_timezones))
6463
		$priority_timezones[] = '-----';
6464
6465
	$timezones = array_merge(
6466
		$priority_timezones,
6467
		array('UTC' => 'UTC' . (!empty($tztxt['UTC']) ? ' - ' . $tztxt['UTC'] : ''), '-----'),
6468
		$timezones
6469
	);
6470
6471
	$timezones_when[$when] = $timezones;
6472
6473
	return $timezones_when[$when];
6474
}
6475
6476
/**
6477
 * Gets a member's selected time zone identifier
6478
 *
6479
 * @param int $id_member The member id to look up. If not provided, the current user's id will be used.
6480
 * @return string The time zone identifier string for the user's time zone.
6481
 */
6482
function getUserTimezone($id_member = null)
6483
{
6484
	global $smcFunc, $context, $user_info, $modSettings, $user_settings;
6485
	static $member_cache = array();
6486
6487
	if (is_null($id_member) && $user_info['is_guest'] == false)
6488
		$id_member = $context['user']['id'];
6489
6490
	// Did we already look this up?
6491
	if (isset($id_member) && isset($member_cache[$id_member]))
6492
	{
6493
		return $member_cache[$id_member];
6494
	}
6495
6496
	// Check if we already have this in $user_settings.
6497
	if (isset($user_settings['id_member']) && $user_settings['id_member'] == $id_member && !empty($user_settings['timezone']))
6498
	{
6499
		$member_cache[$id_member] = $user_settings['timezone'];
6500
		return $user_settings['timezone'];
6501
	}
6502
6503
	// Look it up in the database.
6504
	if (isset($id_member))
6505
	{
6506
		$request = $smcFunc['db_query']('', '
6507
			SELECT timezone
6508
			FROM {db_prefix}members
6509
			WHERE id_member = {int:id_member}',
6510
			array(
6511
				'id_member' => $id_member,
6512
			)
6513
		);
6514
		list($timezone) = $smcFunc['db_fetch_row']($request);
6515
		$smcFunc['db_free_result']($request);
6516
	}
6517
6518
	// If it is invalid, fall back to the default.
6519
	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

6519
	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...
6520
		$timezone = isset($modSettings['default_timezone']) ? $modSettings['default_timezone'] : date_default_timezone_get();
6521
6522
	// Save for later.
6523
	if (isset($id_member))
6524
		$member_cache[$id_member] = $timezone;
6525
6526
	return $timezone;
6527
}
6528
6529
/**
6530
 * Converts an IP address into binary
6531
 *
6532
 * @param string $ip_address An IP address in IPv4, IPv6 or decimal notation
6533
 * @return string|false The IP address in binary or false
6534
 */
6535
function inet_ptod($ip_address)
6536
{
6537
	if (!isValidIP($ip_address))
6538
		return $ip_address;
6539
6540
	$bin = inet_pton($ip_address);
6541
	return $bin;
6542
}
6543
6544
/**
6545
 * Converts a binary version of an IP address into a readable format
6546
 *
6547
 * @param string $bin An IP address in IPv4, IPv6 (Either string (postgresql) or binary (other databases))
6548
 * @return string|false The IP address in presentation format or false on error
6549
 */
6550
function inet_dtop($bin)
6551
{
6552
	global $db_type;
6553
6554
	if (empty($bin))
6555
		return '';
6556
	elseif ($db_type == 'postgresql')
6557
		return $bin;
6558
	// Already a String?
6559
	elseif (isValidIP($bin))
6560
		return $bin;
6561
	return inet_ntop($bin);
6562
}
6563
6564
/**
6565
 * Safe serialize() and unserialize() replacements
6566
 *
6567
 * @license Public Domain
6568
 *
6569
 * @author anthon (dot) pang (at) gmail (dot) com
6570
 */
6571
6572
/**
6573
 * Safe serialize() replacement. Recursive
6574
 * - output a strict subset of PHP's native serialized representation
6575
 * - does not serialize objects
6576
 *
6577
 * @param mixed $value
6578
 * @return string
6579
 */
6580
function _safe_serialize($value)
6581
{
6582
	if (is_null($value))
6583
		return 'N;';
6584
6585
	if (is_bool($value))
6586
		return 'b:' . (int) $value . ';';
6587
6588
	if (is_int($value))
6589
		return 'i:' . $value . ';';
6590
6591
	if (is_float($value))
6592
		return 'd:' . str_replace(',', '.', $value) . ';';
6593
6594
	if (is_string($value))
6595
		return 's:' . strlen($value) . ':"' . $value . '";';
6596
6597
	if (is_array($value))
6598
	{
6599
		// Check for nested objects or resources.
6600
		$contains_invalid = false;
6601
		array_walk_recursive(
6602
			$value,
6603
			function($v) use (&$contains_invalid)
6604
			{
6605
				if (is_object($v) || is_resource($v))
6606
					$contains_invalid = true;
6607
			}
6608
		);
6609
		if ($contains_invalid)
6610
			return false;
6611
6612
		$out = '';
6613
		foreach ($value as $k => $v)
6614
			$out .= _safe_serialize($k) . _safe_serialize($v);
6615
6616
		return 'a:' . count($value) . ':{' . $out . '}';
6617
	}
6618
6619
	// safe_serialize cannot serialize resources or objects.
6620
	return false;
6621
}
6622
6623
/**
6624
 * Wrapper for _safe_serialize() that handles exceptions and multibyte encoding issues.
6625
 *
6626
 * @param mixed $value
6627
 * @return string
6628
 */
6629
function safe_serialize($value)
6630
{
6631
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
6632
	if (function_exists('mb_internal_encoding') &&
6633
		(((int) ini_get('mbstring.func_overload')) & 2))
6634
	{
6635
		$mbIntEnc = mb_internal_encoding();
6636
		mb_internal_encoding('ASCII');
6637
	}
6638
6639
	$out = _safe_serialize($value);
6640
6641
	if (isset($mbIntEnc))
6642
		mb_internal_encoding($mbIntEnc);
6643
6644
	return $out;
6645
}
6646
6647
/**
6648
 * Safe unserialize() replacement
6649
 * - accepts a strict subset of PHP's native serialized representation
6650
 * - does not unserialize objects
6651
 *
6652
 * @param string $str
6653
 * @return mixed
6654
 * @throw Exception if $str is malformed or contains unsupported types (e.g., resources, objects)
6655
 */
6656
function _safe_unserialize($str)
6657
{
6658
	// Input  is not a string.
6659
	if (empty($str) || !is_string($str))
6660
		return false;
6661
6662
	// The substring 'O:' is used to serialize objects.
6663
	// If it is not present, then there are none in the serialized data.
6664
	if (strpos($str, 'O:') === false)
6665
		return unserialize($str);
6666
6667
	$stack = array();
6668
	$expected = array();
6669
6670
	/*
6671
	 * states:
6672
	 *   0 - initial state, expecting a single value or array
6673
	 *   1 - terminal state
6674
	 *   2 - in array, expecting end of array or a key
6675
	 *   3 - in array, expecting value or another array
6676
	 */
6677
	$state = 0;
6678
	while ($state != 1)
6679
	{
6680
		$type = isset($str[0]) ? $str[0] : '';
6681
		if ($type == '}')
6682
			$str = substr($str, 1);
6683
6684
		elseif ($type == 'N' && $str[1] == ';')
6685
		{
6686
			$value = null;
6687
			$str = substr($str, 2);
6688
		}
6689
		elseif ($type == 'b' && preg_match('/^b:([01]);/', $str, $matches))
6690
		{
6691
			$value = $matches[1] == '1' ? true : false;
6692
			$str = substr($str, 4);
6693
		}
6694
		elseif ($type == 'i' && preg_match('/^i:(-?[0-9]+);(.*)/s', $str, $matches))
6695
		{
6696
			$value = (int) $matches[1];
6697
			$str = $matches[2];
6698
		}
6699
		elseif ($type == 'd' && preg_match('/^d:(-?[0-9]+\.?[0-9]*(E[+-][0-9]+)?);(.*)/s', $str, $matches))
6700
		{
6701
			$value = (float) $matches[1];
6702
			$str = $matches[3];
6703
		}
6704
		elseif ($type == 's' && preg_match('/^s:([0-9]+):"(.*)/s', $str, $matches) && substr($matches[2], (int) $matches[1], 2) == '";')
6705
		{
6706
			$value = substr($matches[2], 0, (int) $matches[1]);
6707
			$str = substr($matches[2], (int) $matches[1] + 2);
6708
		}
6709
		elseif ($type == 'a' && preg_match('/^a:([0-9]+):{(.*)/s', $str, $matches))
6710
		{
6711
			$expectedLength = (int) $matches[1];
6712
			$str = $matches[2];
6713
		}
6714
6715
		// Object or unknown/malformed type.
6716
		else
6717
			return false;
6718
6719
		switch ($state)
6720
		{
6721
			case 3: // In array, expecting value or another array.
6722
				if ($type == 'a')
6723
				{
6724
					$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...
6725
					$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...
6726
					$list = &$list[$key];
6727
					$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...
6728
					$state = 2;
6729
					break;
6730
				}
6731
				if ($type != '}')
6732
				{
6733
					$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...
6734
					$state = 2;
6735
					break;
6736
				}
6737
6738
				// Missing array value.
6739
				return false;
6740
6741
			case 2: // in array, expecting end of array or a key
6742
				if ($type == '}')
6743
				{
6744
					// Array size is less than expected.
6745
					if (count($list) < end($expected))
6746
						return false;
6747
6748
					unset($list);
6749
					$list = &$stack[count($stack) - 1];
6750
					array_pop($stack);
6751
6752
					// Go to terminal state if we're at the end of the root array.
6753
					array_pop($expected);
6754
6755
					if (count($expected) == 0)
6756
						$state = 1;
6757
6758
					break;
6759
				}
6760
6761
				if ($type == 'i' || $type == 's')
6762
				{
6763
					// Array size exceeds expected length.
6764
					if (count($list) >= end($expected))
6765
						return false;
6766
6767
					$key = $value;
6768
					$state = 3;
6769
					break;
6770
				}
6771
6772
				// Illegal array index type.
6773
				return false;
6774
6775
			// Expecting array or value.
6776
			case 0:
6777
				if ($type == 'a')
6778
				{
6779
					$data = array();
6780
					$list = &$data;
6781
					$expected[] = $expectedLength;
6782
					$state = 2;
6783
					break;
6784
				}
6785
6786
				if ($type != '}')
6787
				{
6788
					$data = $value;
6789
					$state = 1;
6790
					break;
6791
				}
6792
6793
				// Not in array.
6794
				return false;
6795
		}
6796
	}
6797
6798
	// Trailing data in input.
6799
	if (!empty($str))
6800
		return false;
6801
6802
	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...
6803
}
6804
6805
/**
6806
 * Wrapper for _safe_unserialize() that handles exceptions and multibyte encoding issue
6807
 *
6808
 * @param string $str
6809
 * @return mixed
6810
 */
6811
function safe_unserialize($str)
6812
{
6813
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
6814
	if (function_exists('mb_internal_encoding') &&
6815
		(((int) ini_get('mbstring.func_overload')) & 0x02))
6816
	{
6817
		$mbIntEnc = mb_internal_encoding();
6818
		mb_internal_encoding('ASCII');
6819
	}
6820
6821
	$out = _safe_unserialize($str);
6822
6823
	if (isset($mbIntEnc))
6824
		mb_internal_encoding($mbIntEnc);
6825
6826
	return $out;
6827
}
6828
6829
/**
6830
 * Tries different modes to make file/dirs writable. Wrapper function for chmod()
6831
 *
6832
 * @param string $file The file/dir full path.
6833
 * @param int $value Not needed, added for legacy reasons.
6834
 * @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.
6835
 */
6836
function smf_chmod($file, $value = 0)
6837
{
6838
	// No file? no checks!
6839
	if (empty($file))
6840
		return false;
6841
6842
	// Already writable?
6843
	if (is_writable($file))
6844
		return true;
6845
6846
	// Do we have a file or a dir?
6847
	$isDir = is_dir($file);
6848
	$isWritable = false;
6849
6850
	// Set different modes.
6851
	$chmodValues = $isDir ? array(0750, 0755, 0775, 0777) : array(0644, 0664, 0666);
6852
6853
	foreach ($chmodValues as $val)
6854
	{
6855
		// If it's writable, break out of the loop.
6856
		if (is_writable($file))
6857
		{
6858
			$isWritable = true;
6859
			break;
6860
		}
6861
6862
		else
6863
			@chmod($file, $val);
6864
	}
6865
6866
	return $isWritable;
6867
}
6868
6869
/**
6870
 * Wrapper function for json_decode() with error handling.
6871
 *
6872
 * @param string $json The string to decode.
6873
 * @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.
6874
 * @param bool $logIt To specify if the error will be logged if theres any.
6875
 * @return array Either an empty array or the decoded data as an array.
6876
 */
6877
function smf_json_decode($json, $returnAsArray = false, $logIt = true)
6878
{
6879
	global $txt;
6880
6881
	// Come on...
6882
	if (empty($json) || !is_string($json))
6883
		return array();
6884
6885
	$returnArray = @json_decode($json, $returnAsArray);
6886
6887
	// PHP 5.3 so no json_last_error_msg()
6888
	switch (json_last_error())
6889
	{
6890
		case JSON_ERROR_NONE:
6891
			$jsonError = false;
6892
			break;
6893
		case JSON_ERROR_DEPTH:
6894
			$jsonError = 'JSON_ERROR_DEPTH';
6895
			break;
6896
		case JSON_ERROR_STATE_MISMATCH:
6897
			$jsonError = 'JSON_ERROR_STATE_MISMATCH';
6898
			break;
6899
		case JSON_ERROR_CTRL_CHAR:
6900
			$jsonError = 'JSON_ERROR_CTRL_CHAR';
6901
			break;
6902
		case JSON_ERROR_SYNTAX:
6903
			$jsonError = 'JSON_ERROR_SYNTAX';
6904
			break;
6905
		case JSON_ERROR_UTF8:
6906
			$jsonError = 'JSON_ERROR_UTF8';
6907
			break;
6908
		default:
6909
			$jsonError = 'unknown';
6910
			break;
6911
	}
6912
6913
	// Something went wrong!
6914
	if (!empty($jsonError) && $logIt)
6915
	{
6916
		// Being a wrapper means we lost our smf_error_handler() privileges :(
6917
		$jsonDebug = debug_backtrace();
6918
		$jsonDebug = $jsonDebug[0];
6919
		loadLanguage('Errors');
6920
6921
		if (!empty($jsonDebug))
6922
			log_error($txt['json_' . $jsonError], 'critical', $jsonDebug['file'], $jsonDebug['line']);
6923
6924
		else
6925
			log_error($txt['json_' . $jsonError], 'critical');
6926
6927
		// Everyone expects an array.
6928
		return array();
6929
	}
6930
6931
	return $returnArray;
6932
}
6933
6934
/**
6935
 * Check the given String if he is a valid IPv4 or IPv6
6936
 * return true or false
6937
 *
6938
 * @param string $IPString
6939
 *
6940
 * @return bool
6941
 */
6942
function isValidIP($IPString)
6943
{
6944
	return filter_var($IPString, FILTER_VALIDATE_IP) !== false;
6945
}
6946
6947
/**
6948
 * Outputs a response.
6949
 * It assumes the data is already a string.
6950
 *
6951
 * @param string $data The data to print
6952
 * @param string $type The content type. Defaults to Json.
6953
 * @return void
6954
 */
6955
function smf_serverResponse($data = '', $type = 'content-type: application/json')
6956
{
6957
	global $db_show_debug, $modSettings;
6958
6959
	// Defensive programming anyone?
6960
	if (empty($data))
6961
		return false;
6962
6963
	// Don't need extra stuff...
6964
	$db_show_debug = false;
6965
6966
	// Kill anything else.
6967
	ob_end_clean();
6968
6969
	if (!empty($modSettings['CompressedOutput']))
6970
		@ob_start('ob_gzhandler');
6971
6972
	else
6973
		ob_start();
6974
6975
	// Set the header.
6976
	header($type);
6977
6978
	// Echo!
6979
	echo $data;
6980
6981
	// Done.
6982
	obExit(false);
6983
}
6984
6985
/**
6986
 * Creates an optimized regex to match all known top level domains.
6987
 *
6988
 * The optimized regex is stored in $modSettings['tld_regex'].
6989
 *
6990
 * To update the stored version of the regex to use the latest list of valid
6991
 * TLDs from iana.org, set the $update parameter to true. Updating can take some
6992
 * time, based on network connectivity, so it should normally only be done by
6993
 * calling this function from a background or scheduled task.
6994
 *
6995
 * If $update is not true, but the regex is missing or invalid, the regex will
6996
 * be regenerated from a hard-coded list of TLDs. This regenerated regex will be
6997
 * overwritten on the next scheduled update.
6998
 *
6999
 * @param bool $update If true, fetch and process the latest official list of TLDs from iana.org.
7000
 */
7001
function set_tld_regex($update = false)
7002
{
7003
	global $sourcedir, $smcFunc, $modSettings;
7004
	static $done = false;
7005
7006
	// If we don't need to do anything, don't
7007
	if (!$update && $done)
7008
		return;
7009
7010
	// Should we get a new copy of the official list of TLDs?
7011
	if ($update)
7012
	{
7013
		$tlds = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt');
7014
		$tlds_md5 = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt.md5');
7015
7016
		/**
7017
		 * If the Internet Assigned Numbers Authority can't be reached, the Internet is GONE!
7018
		 * We're probably running on a server hidden in a bunker deep underground to protect
7019
		 * it from marauding bandits roaming on the surface. We don't want to waste precious
7020
		 * electricity on pointlessly repeating background tasks, so we'll wait until the next
7021
		 * regularly scheduled update to see if civilization has been restored.
7022
		 */
7023
		if ($tlds === false || $tlds_md5 === false)
7024
			$postapocalypticNightmare = true;
7025
7026
		// Make sure nothing went horribly wrong along the way.
7027
		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

7027
		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

7027
		if (md5($tlds) != substr(/** @scrutinizer ignore-type */ $tlds_md5, 0, 32))
Loading history...
7028
			$tlds = array();
7029
	}
7030
	// If we aren't updating and the regex is valid, we're done
7031
	elseif (!empty($modSettings['tld_regex']) && @preg_match('~' . $modSettings['tld_regex'] . '~', null) !== false)
0 ignored issues
show
Bug introduced by
null of type null is incompatible with the type string expected by parameter $subject of preg_match(). ( Ignorable by Annotation )

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

7031
	elseif (!empty($modSettings['tld_regex']) && @preg_match('~' . $modSettings['tld_regex'] . '~', /** @scrutinizer ignore-type */ null) !== false)
Loading history...
7032
	{
7033
		$done = true;
7034
		return;
7035
	}
7036
7037
	// If we successfully got an update, process the list into an array
7038
	if (!empty($tlds))
7039
	{
7040
		// Clean $tlds and convert it to an array
7041
		$tlds = array_filter(
7042
			explode("\n", strtolower($tlds)),
7043
			function($line)
7044
			{
7045
				$line = trim($line);
7046
				if (empty($line) || strlen($line) != strspn($line, 'abcdefghijklmnopqrstuvwxyz0123456789-'))
7047
					return false;
7048
				else
7049
					return true;
7050
			}
7051
		);
7052
7053
		// Convert Punycode to Unicode
7054
		if (!function_exists('idn_to_utf8'))
7055
			require_once($sourcedir . '/Subs-Compat.php');
7056
7057
		foreach ($tlds as &$tld)
7058
			$tld = idn_to_utf8($tld, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
7059
	}
7060
	// Otherwise, use the 2012 list of gTLDs and ccTLDs for now and schedule a background update
7061
	else
7062
	{
7063
		$tlds = array('com', 'net', 'org', 'edu', 'gov', 'mil', 'aero', 'asia', 'biz',
7064
			'cat', 'coop', 'info', 'int', 'jobs', 'mobi', 'museum', 'name', 'post',
7065
			'pro', 'tel', 'travel', 'xxx', 'ac', 'ad', 'ae', 'af', 'ag', 'ai', 'al',
7066
			'am', 'ao', 'aq', 'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd',
7067
			'be', 'bf', 'bg', 'bh', 'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv',
7068
			'bw', 'by', 'bz', 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm',
7069
			'cn', 'co', 'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do',
7070
			'dz', 'ec', 'ee', 'eg', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo',
7071
			'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp',
7072
			'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu',
7073
			'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm', 'jo',
7074
			'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la',
7075
			'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md',
7076
			'me', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp', 'mq', 'mr', 'ms', 'mt',
7077
			'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf', 'ng', 'ni', 'nl',
7078
			'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl',
7079
			'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw',
7080
			'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn',
7081
			'so', 'sr', 'ss', 'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg',
7082
			'th', 'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua',
7083
			'ug', 'uk', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf',
7084
			'ws', 'ye', 'yt', 'za', 'zm', 'zw',
7085
		);
7086
7087
		// Schedule a background update, unless civilization has collapsed and/or we are having connectivity issues.
7088
		if (empty($postapocalypticNightmare))
7089
		{
7090
			$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
7091
				array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
7092
				array('$sourcedir/tasks/UpdateTldRegex.php', 'Update_TLD_Regex', '', 0), array()
7093
			);
7094
		}
7095
	}
7096
7097
	// Tack on some "special use domain names" that aren't in DNS but may possibly resolve.
7098
	// See https://www.iana.org/assignments/special-use-domain-names/ for more info.
7099
	$tlds = array_merge($tlds, array('local', 'onion', 'test'));
7100
7101
	// Get an optimized regex to match all the TLDs
7102
	$tld_regex = build_regex($tlds);
7103
7104
	// Remember the new regex in $modSettings
7105
	updateSettings(array('tld_regex' => $tld_regex));
7106
7107
	// Redundant repetition is redundant
7108
	$done = true;
7109
}
7110
7111
/**
7112
 * Creates optimized regular expressions from an array of strings.
7113
 *
7114
 * An optimized regex built using this function will be much faster than a
7115
 * simple regex built using `implode('|', $strings)` --- anywhere from several
7116
 * times to several orders of magnitude faster.
7117
 *
7118
 * However, the time required to build the optimized regex is approximately
7119
 * equal to the time it takes to execute the simple regex. Therefore, it is only
7120
 * worth calling this function if the resulting regex will be used more than
7121
 * once.
7122
 *
7123
 * Because PHP places an upper limit on the allowed length of a regex, very
7124
 * large arrays of $strings may not fit in a single regex. Normally, the excess
7125
 * strings will simply be dropped. However, if the $returnArray parameter is set
7126
 * to true, this function will build as many regexes as necessary to accommodate
7127
 * everything in $strings and return them in an array. You will need to iterate
7128
 * through all elements of the returned array in order to test all possible
7129
 * matches.
7130
 *
7131
 * @param array $strings An array of strings to make a regex for.
7132
 * @param string $delim An optional delimiter character to pass to preg_quote().
7133
 * @param bool $returnArray If true, returns an array of regexes.
7134
 * @return string|array One or more regular expressions to match any of the input strings.
7135
 */
7136
function build_regex($strings, $delim = null, $returnArray = false)
7137
{
7138
	global $smcFunc;
7139
	static $regexes = array();
7140
7141
	// If it's not an array, there's not much to do. ;)
7142
	if (!is_array($strings))
0 ignored issues
show
introduced by
The condition is_array($strings) is always true.
Loading history...
7143
		return preg_quote(@strval($strings), $delim);
7144
7145
	$regex_key = md5(json_encode(array($strings, $delim, $returnArray)));
7146
7147
	if (isset($regexes[$regex_key]))
7148
		return $regexes[$regex_key];
7149
7150
	// The mb_* functions are faster than the $smcFunc ones, but may not be available
7151
	if (function_exists('mb_internal_encoding') && function_exists('mb_detect_encoding') && function_exists('mb_strlen') && function_exists('mb_substr'))
7152
	{
7153
		if (($string_encoding = mb_detect_encoding(implode(' ', $strings))) !== false)
7154
		{
7155
			$current_encoding = mb_internal_encoding();
7156
			mb_internal_encoding($string_encoding);
7157
		}
7158
7159
		$strlen = 'mb_strlen';
7160
		$substr = 'mb_substr';
7161
	}
7162
	else
7163
	{
7164
		$strlen = $smcFunc['strlen'];
7165
		$substr = $smcFunc['substr'];
7166
	}
7167
7168
	// This recursive function creates the index array from the strings
7169
	$add_string_to_index = function($string, $index) use (&$strlen, &$substr, &$add_string_to_index)
7170
	{
7171
		static $depth = 0;
7172
		$depth++;
7173
7174
		$first = (string) @$substr($string, 0, 1);
7175
7176
		// No first character? That's no good.
7177
		if ($first === '')
7178
		{
7179
			// A nested array? Really? Ugh. Fine.
7180
			if (is_array($string) && $depth < 20)
7181
			{
7182
				foreach ($string as $str)
7183
					$index = $add_string_to_index($str, $index);
7184
			}
7185
7186
			$depth--;
7187
			return $index;
7188
		}
7189
7190
		if (empty($index[$first]))
7191
			$index[$first] = array();
7192
7193
		if ($strlen($string) > 1)
7194
		{
7195
			// Sanity check on recursion
7196
			if ($depth > 99)
7197
				$index[$first][$substr($string, 1)] = '';
7198
7199
			else
7200
				$index[$first] = $add_string_to_index($substr($string, 1), $index[$first]);
7201
		}
7202
		else
7203
			$index[$first][''] = '';
7204
7205
		$depth--;
7206
		return $index;
7207
	};
7208
7209
	// This recursive function turns the index array into a regular expression
7210
	$index_to_regex = function(&$index, $delim) use (&$strlen, &$index_to_regex)
7211
	{
7212
		static $depth = 0;
7213
		$depth++;
7214
7215
		// Absolute max length for a regex is 32768, but we might need wiggle room
7216
		$max_length = 30000;
7217
7218
		$regex = array();
7219
		$length = 0;
7220
7221
		foreach ($index as $key => $value)
7222
		{
7223
			$key_regex = preg_quote($key, $delim);
7224
			$new_key = $key;
7225
7226
			if (empty($value))
7227
				$sub_regex = '';
7228
			else
7229
			{
7230
				$sub_regex = $index_to_regex($value, $delim);
7231
7232
				if (count(array_keys($value)) == 1)
7233
				{
7234
					$new_key_array = explode('(?' . '>', $sub_regex);
7235
					$new_key .= $new_key_array[0];
7236
				}
7237
				else
7238
					$sub_regex = '(?' . '>' . $sub_regex . ')';
7239
			}
7240
7241
			if ($depth > 1)
7242
				$regex[$new_key] = $key_regex . $sub_regex;
7243
			else
7244
			{
7245
				if (($length += strlen($key_regex . $sub_regex) + 1) < $max_length || empty($regex))
7246
				{
7247
					$regex[$new_key] = $key_regex . $sub_regex;
7248
					unset($index[$key]);
7249
				}
7250
				else
7251
					break;
7252
			}
7253
		}
7254
7255
		// Sort by key length and then alphabetically
7256
		uksort(
7257
			$regex,
7258
			function($k1, $k2) use (&$strlen)
7259
			{
7260
				$l1 = $strlen($k1);
7261
				$l2 = $strlen($k2);
7262
7263
				if ($l1 == $l2)
7264
					return strcmp($k1, $k2) > 0 ? 1 : -1;
7265
				else
7266
					return $l1 > $l2 ? -1 : 1;
7267
			}
7268
		);
7269
7270
		$depth--;
7271
		return implode('|', $regex);
7272
	};
7273
7274
	// Now that the functions are defined, let's do this thing
7275
	$index = array();
7276
	$regex = '';
7277
7278
	foreach ($strings as $string)
7279
		$index = $add_string_to_index($string, $index);
7280
7281
	if ($returnArray === true)
7282
	{
7283
		$regex = array();
7284
		while (!empty($index))
7285
			$regex[] = '(?' . '>' . $index_to_regex($index, $delim) . ')';
7286
	}
7287
	else
7288
		$regex = '(?' . '>' . $index_to_regex($index, $delim) . ')';
7289
7290
	// Restore PHP's internal character encoding to whatever it was originally
7291
	if (!empty($current_encoding))
7292
		mb_internal_encoding($current_encoding);
7293
7294
	$regexes[$regex_key] = $regex;
7295
	return $regex;
7296
}
7297
7298
/**
7299
 * Check if the passed url has an SSL certificate.
7300
 *
7301
 * Returns true if a cert was found & false if not.
7302
 *
7303
 * @param string $url to check, in $boardurl format (no trailing slash).
7304
 */
7305
function ssl_cert_found($url)
7306
{
7307
	// This check won't work without OpenSSL
7308
	if (!extension_loaded('openssl'))
7309
		return true;
7310
7311
	// First, strip the subfolder from the passed url, if any
7312
	$parsedurl = parse_url($url);
7313
	$url = 'ssl://' . $parsedurl['host'] . ':443';
7314
7315
	// Next, check the ssl stream context for certificate info
7316
	if (version_compare(PHP_VERSION, '5.6.0', '<'))
7317
		$ssloptions = array("capture_peer_cert" => true);
7318
	else
7319
		$ssloptions = array("capture_peer_cert" => true, "verify_peer" => true, "allow_self_signed" => true);
7320
7321
	$result = false;
7322
	$context = stream_context_create(array("ssl" => $ssloptions));
7323
	$stream = @stream_socket_client($url, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
7324
	if ($stream !== false)
7325
	{
7326
		$params = stream_context_get_params($stream);
7327
		$result = isset($params["options"]["ssl"]["peer_certificate"]) ? true : false;
7328
	}
7329
	return $result;
7330
}
7331
7332
/**
7333
 * Check if the passed url has a redirect to https:// by querying headers.
7334
 *
7335
 * Returns true if a redirect was found & false if not.
7336
 * Note that when force_ssl = 2, SMF issues its own redirect...  So if this
7337
 * returns true, it may be caused by SMF, not necessarily an .htaccess redirect.
7338
 *
7339
 * @param string $url to check, in $boardurl format (no trailing slash).
7340
 */
7341
function https_redirect_active($url)
7342
{
7343
	// Ask for the headers for the passed url, but via http...
7344
	// Need to add the trailing slash, or it puts it there & thinks there's a redirect when there isn't...
7345
	$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

7345
	$url = /** @scrutinizer ignore-type */ str_ireplace('https://', 'http://', $url) . '/';
Loading history...
7346
	$headers = @get_headers($url);
7347
	if ($headers === false)
7348
		return false;
7349
7350
	// Now to see if it came back https...
7351
	// First check for a redirect status code in first row (301, 302, 307)
7352
	if (strstr($headers[0], '301') === false && strstr($headers[0], '302') === false && strstr($headers[0], '307') === false)
7353
		return false;
7354
7355
	// Search for the location entry to confirm https
7356
	$result = false;
7357
	foreach ($headers as $header)
7358
	{
7359
		if (stristr($header, 'Location: https://') !== false)
7360
		{
7361
			$result = true;
7362
			break;
7363
		}
7364
	}
7365
	return $result;
7366
}
7367
7368
/**
7369
 * Build query_wanna_see_board and query_see_board for a userid
7370
 *
7371
 * Returns array with keys query_wanna_see_board and query_see_board
7372
 *
7373
 * @param int $userid of the user
7374
 */
7375
function build_query_board($userid)
7376
{
7377
	global $user_info, $modSettings, $smcFunc, $db_prefix;
7378
7379
	$query_part = array();
7380
7381
	// If we come from cron, we can't have a $user_info.
7382
	if (isset($user_info['id']) && $user_info['id'] == $userid && SMF != 'BACKGROUND')
7383
	{
7384
		$groups = $user_info['groups'];
7385
		$can_see_all_boards = $user_info['is_admin'] || $user_info['can_manage_boards'];
7386
		$ignoreboards = !empty($user_info['ignoreboards']) ? $user_info['ignoreboards'] : null;
7387
	}
7388
	else
7389
	{
7390
		$request = $smcFunc['db_query']('', '
7391
			SELECT mem.ignore_boards, mem.id_group, mem.additional_groups, mem.id_post_group
7392
			FROM {db_prefix}members AS mem
7393
			WHERE mem.id_member = {int:id_member}
7394
			LIMIT 1',
7395
			array(
7396
				'id_member' => $userid,
7397
			)
7398
		);
7399
7400
		$row = $smcFunc['db_fetch_assoc']($request);
7401
7402
		if (empty($row['additional_groups']))
7403
			$groups = array($row['id_group'], $row['id_post_group']);
7404
		else
7405
			$groups = array_merge(
7406
				array($row['id_group'], $row['id_post_group']),
7407
				explode(',', $row['additional_groups'])
7408
			);
7409
7410
		// Because history has proven that it is possible for groups to go bad - clean up in case.
7411
		foreach ($groups as $k => $v)
7412
			$groups[$k] = (int) $v;
7413
7414
		$can_see_all_boards = in_array(1, $groups) || (!empty($modSettings['board_manager_groups']) && count(array_intersect($groups, explode(',', $modSettings['board_manager_groups']))) > 0);
7415
7416
		$ignoreboards = !empty($row['ignore_boards']) && !empty($modSettings['allow_ignore_boards']) ? explode(',', $row['ignore_boards']) : array();
7417
	}
7418
7419
	// Just build this here, it makes it easier to change/use - administrators can see all boards.
7420
	if ($can_see_all_boards)
7421
		$query_part['query_see_board'] = '1=1';
7422
	// Otherwise just the groups in $user_info['groups'].
7423
	else
7424
	{
7425
		$query_part['query_see_board'] = '
7426
			EXISTS (
7427
				SELECT bpv.id_board
7428
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
7429
				WHERE bpv.id_group IN ('. implode(',', $groups) .')
7430
					AND bpv.deny = 0
7431
					AND bpv.id_board = b.id_board
7432
			)';
7433
7434
		if (!empty($modSettings['deny_boards_access']))
7435
			$query_part['query_see_board'] .= '
7436
			AND NOT EXISTS (
7437
				SELECT bpv.id_board
7438
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
7439
				WHERE bpv.id_group IN ( '. implode(',', $groups) .')
7440
					AND bpv.deny = 1
7441
					AND bpv.id_board = b.id_board
7442
			)';
7443
	}
7444
7445
	$query_part['query_see_message_board'] = str_replace('b.', 'm.', $query_part['query_see_board']);
7446
	$query_part['query_see_topic_board'] = str_replace('b.', 't.', $query_part['query_see_board']);
7447
7448
	// Build the list of boards they WANT to see.
7449
	// This will take the place of query_see_boards in certain spots, so it better include the boards they can see also
7450
7451
	// If they aren't ignoring any boards then they want to see all the boards they can see
7452
	if (empty($ignoreboards))
7453
	{
7454
		$query_part['query_wanna_see_board'] = $query_part['query_see_board'];
7455
		$query_part['query_wanna_see_message_board'] = $query_part['query_see_message_board'];
7456
		$query_part['query_wanna_see_topic_board'] = $query_part['query_see_topic_board'];
7457
	}
7458
	// Ok I guess they don't want to see all the boards
7459
	else
7460
	{
7461
		$query_part['query_wanna_see_board'] = '(' . $query_part['query_see_board'] . ' AND b.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7462
		$query_part['query_wanna_see_message_board'] = '(' . $query_part['query_see_message_board'] . ' AND m.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7463
		$query_part['query_wanna_see_topic_board'] = '(' . $query_part['query_see_topic_board'] . ' AND t.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7464
	}
7465
7466
	return $query_part;
7467
}
7468
7469
/**
7470
 * Check if the connection is using https.
7471
 *
7472
 * @return boolean true if connection used https
7473
 */
7474
function httpsOn()
7475
{
7476
	$secure = false;
7477
7478
	if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on')
7479
		$secure = true;
7480
	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...
7481
		$secure = true;
7482
7483
	return $secure;
7484
}
7485
7486
/**
7487
 * A wrapper for `filter_var($url, FILTER_VALIDATE_URL)` that can handle URLs
7488
 * with international characters (a.k.a. IRIs)
7489
 *
7490
 * @param string $iri The IRI to test.
7491
 * @param int $flags Optional flags to pass to filter_var()
7492
 * @return string|bool Either the original IRI, or false if the IRI was invalid.
7493
 */
7494
function validate_iri($iri, $flags = null)
7495
{
7496
	$url = iri_to_url($iri);
7497
7498
	// PHP 5 doesn't recognize IPv6 addresses in the URL host.
7499
	if (version_compare(phpversion(), '7.0.0', '<'))
7500
	{
7501
		$host = parse_url((strpos($url, '//') === 0 ? 'http:' : '') . $url, PHP_URL_HOST);
7502
7503
		if (strpos($host, '[') === 0 && strpos($host, ']') === strlen($host) - 1 && strpos($host, ':') !== false)
7504
			$url = str_replace($host, '127.0.0.1', $url);
7505
	}
7506
7507
	if (filter_var($url, FILTER_VALIDATE_URL, $flags) !== false)
0 ignored issues
show
Bug introduced by
It seems like $flags can also be of type null; however, parameter $options of filter_var() does only seem to accept array|integer, maybe add an additional type check? ( Ignorable by Annotation )

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

7507
	if (filter_var($url, FILTER_VALIDATE_URL, /** @scrutinizer ignore-type */ $flags) !== false)
Loading history...
7508
		return $iri;
7509
	else
7510
		return false;
7511
}
7512
7513
/**
7514
 * A wrapper for `filter_var($url, FILTER_SANITIZE_URL)` that can handle URLs
7515
 * with international characters (a.k.a. IRIs)
7516
 *
7517
 * Note: The returned value will still be an IRI, not a URL. To convert to URL,
7518
 * feed the result of this function to iri_to_url()
7519
 *
7520
 * @param string $iri The IRI to sanitize.
7521
 * @return string|bool The sanitized version of the IRI
7522
 */
7523
function sanitize_iri($iri)
7524
{
7525
	// Encode any non-ASCII characters (but not space or control characters of any sort)
7526
	// Also encode '%' in order to preserve anything that is already percent-encoded.
7527
	$iri = preg_replace_callback(
7528
		'~[^\x00-\x7F\pZ\pC]|%~u',
7529
		function($matches)
7530
		{
7531
			return rawurlencode($matches[0]);
7532
		},
7533
		$iri
7534
	);
7535
7536
	// Perform normal sanitization
7537
	$iri = filter_var($iri, FILTER_SANITIZE_URL);
7538
7539
	// Decode the non-ASCII characters
7540
	$iri = rawurldecode($iri);
7541
7542
	return $iri;
7543
}
7544
7545
/**
7546
 * Converts a URL with international characters (an IRI) into a pure ASCII URL
7547
 *
7548
 * Uses Punycode to encode any non-ASCII characters in the domain name, and uses
7549
 * standard URL encoding on the rest.
7550
 *
7551
 * @param string $iri A IRI that may or may not contain non-ASCII characters.
7552
 * @return string|bool The URL version of the IRI.
7553
 */
7554
function iri_to_url($iri)
7555
{
7556
	global $sourcedir;
7557
7558
	$host = parse_url((strpos($iri, '//') === 0 ? 'http:' : '') . $iri, PHP_URL_HOST);
7559
7560
	if (!empty($host))
7561
	{
7562
		if (!function_exists('idn_to_ascii'))
7563
			require_once($sourcedir . '/Subs-Compat.php');
7564
7565
		// Convert the host using the Punycode algorithm
7566
		$encoded_host = idn_to_ascii($host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
7567
7568
		$pos = strpos($iri, $host);
7569
	}
7570
	else
7571
	{
7572
		$encoded_host = '';
7573
		$pos = 0;
7574
	}
7575
7576
	$before_host = substr($iri, 0, $pos);
7577
	$after_host = substr($iri, $pos + strlen($host));
7578
7579
	// Encode any disallowed characters in the rest of the URL
7580
	$unescaped = array(
7581
		'%21' => '!', '%23' => '#', '%24' => '$', '%26' => '&',
7582
		'%27' => "'", '%28' => '(', '%29' => ')', '%2A' => '*',
7583
		'%2B' => '+', '%2C' => ',', '%2F' => '/', '%3A' => ':',
7584
		'%3B' => ';', '%3D' => '=', '%3F' => '?', '%40' => '@',
7585
		'%25' => '%',
7586
	);
7587
7588
	$before_host = strtr(rawurlencode($before_host), $unescaped);
7589
	$after_host = strtr(rawurlencode($after_host), $unescaped);
7590
7591
	return $before_host . $encoded_host . $after_host;
7592
}
7593
7594
/**
7595
 * Decodes a URL containing encoded international characters to UTF-8
7596
 *
7597
 * Decodes any Punycode encoded characters in the domain name, then uses
7598
 * standard URL decoding on the rest.
7599
 *
7600
 * @param string $url The pure ASCII version of a URL.
7601
 * @return string|bool The UTF-8 version of the URL.
7602
 */
7603
function url_to_iri($url)
7604
{
7605
	global $sourcedir;
7606
7607
	$host = parse_url((strpos($url, '//') === 0 ? 'http:' : '') . $url, PHP_URL_HOST);
7608
7609
	if (!empty($host))
7610
	{
7611
		if (!function_exists('idn_to_utf8'))
7612
			require_once($sourcedir . '/Subs-Compat.php');
7613
7614
		// Decode the domain from Punycode
7615
		$decoded_host = idn_to_utf8($host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
7616
7617
		$pos = strpos($url, $host);
7618
	}
7619
	else
7620
	{
7621
		$decoded_host = '';
7622
		$pos = 0;
7623
	}
7624
7625
	$before_host = substr($url, 0, $pos);
7626
	$after_host = substr($url, $pos + strlen($host));
7627
7628
	// Decode the rest of the URL
7629
	$before_host = rawurldecode($before_host);
7630
	$after_host = rawurldecode($after_host);
7631
7632
	return $before_host . $decoded_host . $after_host;
7633
}
7634
7635
/**
7636
 * Ensures SMF's scheduled tasks are being run as intended
7637
 *
7638
 * If the admin activated the cron_is_real_cron setting, but the cron job is
7639
 * not running things at least once per day, we need to go back to SMF's default
7640
 * behaviour using "web cron" JavaScript calls.
7641
 */
7642
function check_cron()
7643
{
7644
	global $modSettings, $smcFunc, $txt;
7645
7646
	if (!empty($modSettings['cron_is_real_cron']) && time() - @intval($modSettings['cron_last_checked']) > 84600)
7647
	{
7648
		$request = $smcFunc['db_query']('', '
7649
			SELECT COUNT(*)
7650
			FROM {db_prefix}scheduled_tasks
7651
			WHERE disabled = {int:not_disabled}
7652
				AND next_time < {int:yesterday}',
7653
			array(
7654
				'not_disabled' => 0,
7655
				'yesterday' => time() - 84600,
7656
			)
7657
		);
7658
		list($overdue) = $smcFunc['db_fetch_row']($request);
7659
		$smcFunc['db_free_result']($request);
7660
7661
		// If we have tasks more than a day overdue, cron isn't doing its job.
7662
		if (!empty($overdue))
7663
		{
7664
			loadLanguage('ManageScheduledTasks');
7665
			log_error($txt['cron_not_working']);
7666
			updateSettings(array('cron_is_real_cron' => 0));
7667
		}
7668
		else
7669
			updateSettings(array('cron_last_checked' => time()));
7670
	}
7671
}
7672
7673
/**
7674
 * Sends an appropriate HTTP status header based on a given status code
7675
 *
7676
 * @param int $code The status code
7677
 * @param string $status The string for the status. Set automatically if not provided.
7678
 */
7679
function send_http_status($code, $status = '')
7680
{
7681
	global $sourcedir;
7682
7683
	$statuses = array(
7684
		204 => 'No Content',
7685
		206 => 'Partial Content',
7686
		304 => 'Not Modified',
7687
		400 => 'Bad Request',
7688
		403 => 'Forbidden',
7689
		404 => 'Not Found',
7690
		410 => 'Gone',
7691
		500 => 'Internal Server Error',
7692
		503 => 'Service Unavailable',
7693
	);
7694
7695
	$protocol = preg_match('~^\s*(HTTP/[12]\.\d)\s*$~i', $_SERVER['SERVER_PROTOCOL'], $matches) ? $matches[1] : 'HTTP/1.0';
7696
7697
	// Typically during these requests, we have cleaned the response (ob_*clean), ensure these headers exist.
7698
	require_once($sourcedir . '/Security.php');
7699
	frameOptionsHeader();
7700
	corsPolicyHeader();
7701
7702
	if (!isset($statuses[$code]) && empty($status))
7703
		header($protocol . ' 500 Internal Server Error');
7704
	else
7705
		header($protocol . ' ' . $code . ' ' . (!empty($status) ? $status : $statuses[$code]));
7706
}
7707
7708
/**
7709
 * Concatenates an array of strings into a grammatically correct sentence list
7710
 *
7711
 * Uses formats defined in the language files to build the list appropropriately
7712
 * for the currently loaded language.
7713
 *
7714
 * @param array $list An array of strings to concatenate.
7715
 * @return string The localized sentence list.
7716
 */
7717
function sentence_list($list)
7718
{
7719
	global $txt;
7720
7721
	// Make sure the bare necessities are defined
7722
	if (empty($txt['sentence_list_format']['n']))
7723
		$txt['sentence_list_format']['n'] = '{series}';
7724
	if (!isset($txt['sentence_list_separator']))
7725
		$txt['sentence_list_separator'] = ', ';
7726
	if (!isset($txt['sentence_list_separator_alt']))
7727
		$txt['sentence_list_separator_alt'] = '; ';
7728
7729
	// Which format should we use?
7730
	if (isset($txt['sentence_list_format'][count($list)]))
7731
		$format = $txt['sentence_list_format'][count($list)];
7732
	else
7733
		$format = $txt['sentence_list_format']['n'];
7734
7735
	// Do we want the normal separator or the alternate?
7736
	$separator = $txt['sentence_list_separator'];
7737
	foreach ($list as $item)
7738
	{
7739
		if (strpos($item, $separator) !== false)
7740
		{
7741
			$separator = $txt['sentence_list_separator_alt'];
7742
			$format = strtr($format, trim($txt['sentence_list_separator']), trim($separator));
7743
			break;
7744
		}
7745
	}
7746
7747
	$replacements = array();
7748
7749
	// Special handling for the last items on the list
7750
	$i = 0;
7751
	while (empty($done))
7752
	{
7753
		if (strpos($format, '{'. --$i . '}') !== false)
7754
			$replacements['{'. $i . '}'] = array_pop($list);
7755
		else
7756
			$done = true;
7757
	}
7758
	unset($done);
7759
7760
	// Special handling for the first items on the list
7761
	$i = 0;
7762
	while (empty($done))
7763
	{
7764
		if (strpos($format, '{'. ++$i . '}') !== false)
7765
			$replacements['{'. $i . '}'] = array_shift($list);
7766
		else
7767
			$done = true;
7768
	}
7769
	unset($done);
7770
7771
	// Whatever is left
7772
	$replacements['{series}'] = implode($separator, $list);
7773
7774
	// Do the deed
7775
	return strtr($format, $replacements);
7776
}
7777
7778
/**
7779
 * Truncate an array to a specified length
7780
 *
7781
 * @param array $array The array to truncate
7782
 * @param int $max_length The upperbound on the length
7783
 * @param int $deep How levels in an multidimensional array should the function take into account.
7784
 * @return array The truncated array
7785
 */
7786
function truncate_array($array, $max_length = 1900, $deep = 3)
7787
{
7788
	$array = (array) $array;
7789
7790
	$curr_length = array_length($array, $deep);
7791
7792
	if ($curr_length <= $max_length)
7793
		return $array;
7794
7795
	else
7796
	{
7797
		// Truncate each element's value to a reasonable length
7798
		$param_max = floor($max_length / count($array));
7799
7800
		$current_deep = $deep - 1;
7801
7802
		foreach ($array as $key => &$value)
7803
		{
7804
			if (is_array($value))
7805
				if ($current_deep > 0)
7806
					$value = truncate_array($value, $current_deep);
7807
7808
			else
7809
				$value = substr($value, 0, $param_max - strlen($key) - 5);
0 ignored issues
show
Bug introduced by
$param_max - strlen($key) - 5 of type double is incompatible with the type integer|null expected by parameter $length of substr(). ( Ignorable by Annotation )

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

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

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

7809
				$value = substr(/** @scrutinizer ignore-type */ $value, 0, $param_max - strlen($key) - 5);
Loading history...
7810
		}
7811
7812
		return $array;
7813
	}
7814
}
7815
7816
/**
7817
 * array_length Recursive
7818
 * @param array $array
7819
 * @param int $deep How many levels should the function
7820
 * @return int
7821
 */
7822
function array_length($array, $deep = 3)
7823
{
7824
	// Work with arrays
7825
	$array = (array) $array;
7826
	$length = 0;
7827
7828
	$deep_count = $deep - 1;
7829
7830
	foreach ($array as $value)
7831
	{
7832
		// Recursive?
7833
		if (is_array($value))
7834
		{
7835
			// No can't do
7836
			if ($deep_count <= 0)
7837
				continue;
7838
7839
			$length += array_length($value, $deep_count);
7840
		}
7841
		else
7842
			$length += strlen($value);
7843
	}
7844
7845
	return $length;
7846
}
7847
7848
/**
7849
 * Compares existance request variables against an array.
7850
 *
7851
 * The input array is associative, where keys denote accepted values
7852
 * in a request variable denoted by `$req_val`. Values can be:
7853
 *
7854
 * - another associative array where at least one key must be found
7855
 *   in the request and their values are accepted request values.
7856
 * - A scalar value, in which case no furthur checks are done.
7857
 *
7858
 * @param array $array
7859
 * @param string $req_var request variable
7860
 *
7861
 * @return bool whether any of the criteria was satisfied
7862
 */
7863
function is_filtered_request(array $array, $req_var)
7864
{
7865
	$matched = false;
7866
	if (isset($_REQUEST[$req_var], $array[$_REQUEST[$req_var]]))
7867
	{
7868
		if (is_array($array[$_REQUEST[$req_var]]))
7869
		{
7870
			foreach ($array[$_REQUEST[$req_var]] as $subtype => $subnames)
7871
				$matched |= isset($_REQUEST[$subtype]) && in_array($_REQUEST[$subtype], $subnames);
7872
		}
7873
		else
7874
			$matched = true;
7875
	}
7876
7877
	return (bool) $matched;
7878
}
7879
7880
/**
7881
 * Clean up the XML to make sure it doesn't contain invalid characters.
7882
 *
7883
 * See https://www.w3.org/TR/xml/#charsets
7884
 *
7885
 * @param string $string The string to clean
7886
 * @return string The cleaned string
7887
 */
7888
function cleanXml($string)
7889
{
7890
	global $context;
7891
7892
	$illegal_chars = array(
7893
		// Remove all ASCII control characters except \t, \n, and \r.
7894
		"\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08",
7895
		"\x0B", "\x0C", "\x0E", "\x0F", "\x10", "\x11", "\x12", "\x13", "\x14",
7896
		"\x15", "\x16", "\x17", "\x18", "\x19", "\x1A", "\x1B", "\x1C", "\x1D",
7897
		"\x1E", "\x1F",
7898
		// Remove \xFFFE and \xFFFF
7899
		"\xEF\xBF\xBE", "\xEF\xBF\xBF",
7900
	);
7901
7902
	$string = str_replace($illegal_chars, '', $string);
7903
7904
	// The Unicode surrogate pair code points should never be present in our
7905
	// strings to begin with, but if any snuck in, they need to be removed.
7906
	if (!empty($context['utf8']) && strpos($string, "\xED") !== false)
7907
		$string = preg_replace('/\xED[\xA0-\xBF][\x80-\xBF]/', '', $string);
7908
7909
	return $string;
7910
}
7911
7912
/**
7913
 * Escapes (replaces) characters in strings to make them safe for use in javascript
7914
 *
7915
 * @param string $string The string to escape
7916
 * @return string The escaped string
7917
 */
7918
function JavaScriptEscape($string)
7919
{
7920
	global $scripturl;
7921
7922
	return '\'' . strtr($string, array(
7923
		"\r" => '',
7924
		"\n" => '\\n',
7925
		"\t" => '\\t',
7926
		'\\' => '\\\\',
7927
		'\'' => '\\\'',
7928
		'</' => '<\' + \'/',
7929
		'<script' => '<scri\'+\'pt',
7930
		'<body>' => '<bo\'+\'dy>',
7931
		'<a href' => '<a hr\'+\'ef',
7932
		$scripturl => '\' + smf_scripturl + \'',
7933
	)) . '\'';
7934
}
7935
7936
function tokenTxtReplace($stringSubject = '')
7937
{
7938
	global $txt;
7939
7940
	if (empty($stringSubject))
7941
		return '';
7942
7943
	$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...
7944
	$toFind = array();
7945
	$replaceWith = array();
7946
7947
	if (!empty($matches[1]))
7948
		foreach ($matches[1] as $token) {
7949
			$toFind[] = '{' . $token . '}';
7950
			$replaceWith[] = isset($txt[$token]) ? $txt[$token] : $token;
7951
		}
7952
7953
	return str_replace($toFind, $replaceWith, $stringSubject);
7954
}
7955
7956
?>