Passed
Push — release-2.1 ( 160bdd...a13834 )
by Jon
05:15 queued 10s
created

smf_list_timezones()   F

Complexity

Conditions 48

Size

Total Lines 197
Code Lines 107

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 48
eloc 107
c 3
b 1
f 0
nop 1
dl 0
loc 197
rs 3.3333

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 2020 Simple Machines and individual contributors
11
 * @license https://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1 RC3
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;
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
			trigger_error('updateStats(): Invalid statistic type \'' . $type . '\'', E_USER_NOTICE);
246
	}
247
}
248
249
/**
250
 * Updates the columns in the members table.
251
 * Assumes the data has been htmlspecialchar'd.
252
 * this function should be used whenever member data needs to be
253
 * updated in place of an UPDATE query.
254
 *
255
 * id_member is either an int or an array of ints to be updated.
256
 *
257
 * data is an associative array of the columns to be updated and their respective values.
258
 * any string values updated should be quoted and slashed.
259
 *
260
 * the value of any column can be '+' or '-', which mean 'increment'
261
 * and decrement, respectively.
262
 *
263
 * if the member's post number is updated, updates their post groups.
264
 *
265
 * @param mixed $members An array of member IDs, the ID of a single member, or null to update this for all members
266
 * @param array $data The info to update for the members
267
 */
268
function updateMemberData($members, $data)
269
{
270
	global $modSettings, $user_info, $smcFunc, $sourcedir, $cache_enable;
271
272
	// An empty array means there's nobody to update.
273
	if ($members === array())
274
		return;
275
276
	$parameters = array();
277
	if (is_array($members))
278
	{
279
		$condition = 'id_member IN ({array_int:members})';
280
		$parameters['members'] = $members;
281
	}
282
283
	elseif ($members === null)
284
		$condition = '1=1';
285
286
	else
287
	{
288
		$condition = 'id_member = {int:member}';
289
		$parameters['member'] = $members;
290
	}
291
292
	// Everything is assumed to be a string unless it's in the below.
293
	$knownInts = array(
294
		'date_registered', 'posts', 'id_group', 'last_login', 'instant_messages', 'unread_messages',
295
		'new_pm', 'pm_prefs', 'gender', 'show_online', 'pm_receive_from', 'alerts',
296
		'id_theme', 'is_activated', 'id_msg_last_visit', 'id_post_group', 'total_time_logged_in', 'warning',
297
	);
298
	$knownFloats = array(
299
		'time_offset',
300
	);
301
302
	if (!empty($modSettings['integrate_change_member_data']))
303
	{
304
		// Only a few member variables are really interesting for integration.
305
		$integration_vars = array(
306
			'member_name',
307
			'real_name',
308
			'email_address',
309
			'id_group',
310
			'gender',
311
			'birthdate',
312
			'website_title',
313
			'website_url',
314
			'location',
315
			'time_format',
316
			'time_offset',
317
			'avatar',
318
			'lngfile',
319
		);
320
		$vars_to_integrate = array_intersect($integration_vars, array_keys($data));
321
322
		// Only proceed if there are any variables left to call the integration function.
323
		if (count($vars_to_integrate) != 0)
324
		{
325
			// Fetch a list of member_names if necessary
326
			if ((!is_array($members) && $members === $user_info['id']) || (is_array($members) && count($members) == 1 && in_array($user_info['id'], $members)))
327
				$member_names = array($user_info['username']);
328
			else
329
			{
330
				$member_names = array();
331
				$request = $smcFunc['db_query']('', '
332
					SELECT member_name
333
					FROM {db_prefix}members
334
					WHERE ' . $condition,
335
					$parameters
336
				);
337
				while ($row = $smcFunc['db_fetch_assoc']($request))
338
					$member_names[] = $row['member_name'];
339
				$smcFunc['db_free_result']($request);
340
			}
341
342
			if (!empty($member_names))
343
				foreach ($vars_to_integrate as $var)
344
					call_integration_hook('integrate_change_member_data', array($member_names, $var, &$data[$var], &$knownInts, &$knownFloats));
345
		}
346
	}
347
348
	$setString = '';
349
	foreach ($data as $var => $val)
350
	{
351
		switch ($var)
352
		{
353
			case  'birthdate':
354
				$type = 'date';
355
				break;
356
357
			case 'member_ip':
358
			case 'member_ip2':
359
				$type = 'inet';
360
				break;
361
362
			default:
363
				$type = 'string';
364
		}
365
366
		if (in_array($var, $knownInts))
367
			$type = 'int';
368
369
		elseif (in_array($var, $knownFloats))
370
			$type = 'float';
371
372
		// Doing an increment?
373
		if ($var == 'alerts' && ($val === '+' || $val === '-'))
374
		{
375
			include_once($sourcedir . '/Profile-Modify.php');
376
			if (is_array($members))
377
			{
378
				$val = 'CASE ';
379
				foreach ($members as $k => $v)
380
					$val .= 'WHEN id_member = ' . $v . ' THEN '. alert_count($v, true) . ' ';
381
382
				$val = $val . ' END';
383
				$type = 'raw';
384
			}
385
386
			else
387
				$val = alert_count($members, true);
388
		}
389
390
		elseif ($type == 'int' && ($val === '+' || $val === '-'))
391
		{
392
			$val = $var . ' ' . $val . ' 1';
393
			$type = 'raw';
394
		}
395
396
		// Ensure posts, instant_messages, and unread_messages don't overflow or underflow.
397
		if (in_array($var, array('posts', 'instant_messages', 'unread_messages')))
398
		{
399
			if (preg_match('~^' . $var . ' (\+ |- |\+ -)([\d]+)~', $val, $match))
400
			{
401
				if ($match[1] != '+ ')
402
					$val = 'CASE WHEN ' . $var . ' <= ' . abs($match[2]) . ' THEN 0 ELSE ' . $val . ' END';
403
404
				$type = 'raw';
405
			}
406
		}
407
408
		$setString .= ' ' . $var . ' = {' . $type . ':p_' . $var . '},';
409
		$parameters['p_' . $var] = $val;
410
	}
411
412
	$smcFunc['db_query']('', '
413
		UPDATE {db_prefix}members
414
		SET' . substr($setString, 0, -1) . '
415
		WHERE ' . $condition,
416
		$parameters
417
	);
418
419
	updateStats('postgroups', $members, array_keys($data));
420
421
	// Clear any caching?
422
	if (!empty($cache_enable) && $cache_enable >= 2 && !empty($members))
423
	{
424
		if (!is_array($members))
425
			$members = array($members);
426
427
		foreach ($members as $member)
428
		{
429
			if ($cache_enable >= 3)
430
			{
431
				cache_put_data('member_data-profile-' . $member, null, 120);
432
				cache_put_data('member_data-normal-' . $member, null, 120);
433
				cache_put_data('member_data-minimal-' . $member, null, 120);
434
			}
435
			cache_put_data('user_settings-' . $member, null, 60);
436
		}
437
	}
438
}
439
440
/**
441
 * Updates the settings table as well as $modSettings... only does one at a time if $update is true.
442
 *
443
 * - updates both the settings table and $modSettings array.
444
 * - all of changeArray's indexes and values are assumed to have escaped apostrophes (')!
445
 * - if a variable is already set to what you want to change it to, that
446
 *   variable will be skipped over; it would be unnecessary to reset.
447
 * - When use_update is true, UPDATEs will be used instead of REPLACE.
448
 * - when use_update is true, the value can be true or false to increment
449
 *  or decrement it, respectively.
450
 *
451
 * @param array $changeArray An array of info about what we're changing in 'setting' => 'value' format
452
 * @param bool $update Whether to use an UPDATE query instead of a REPLACE query
453
 */
454
function updateSettings($changeArray, $update = false)
455
{
456
	global $modSettings, $smcFunc;
457
458
	if (empty($changeArray) || !is_array($changeArray))
459
		return;
460
461
	$toRemove = array();
462
463
	// Go check if there is any setting to be removed.
464
	foreach ($changeArray as $k => $v)
465
		if ($v === null)
466
		{
467
			// Found some, remove them from the original array and add them to ours.
468
			unset($changeArray[$k]);
469
			$toRemove[] = $k;
470
		}
471
472
	// Proceed with the deletion.
473
	if (!empty($toRemove))
474
		$smcFunc['db_query']('', '
475
			DELETE FROM {db_prefix}settings
476
			WHERE variable IN ({array_string:remove})',
477
			array(
478
				'remove' => $toRemove,
479
			)
480
		);
481
482
	// In some cases, this may be better and faster, but for large sets we don't want so many UPDATEs.
483
	if ($update)
484
	{
485
		foreach ($changeArray as $variable => $value)
486
		{
487
			$smcFunc['db_query']('', '
488
				UPDATE {db_prefix}settings
489
				SET value = {' . ($value === false || $value === true ? 'raw' : 'string') . ':value}
490
				WHERE variable = {string:variable}',
491
				array(
492
					'value' => $value === true ? 'value + 1' : ($value === false ? 'value - 1' : $value),
493
					'variable' => $variable,
494
				)
495
			);
496
			$modSettings[$variable] = $value === true ? $modSettings[$variable] + 1 : ($value === false ? $modSettings[$variable] - 1 : $value);
497
		}
498
499
		// Clean out the cache and make sure the cobwebs are gone too.
500
		cache_put_data('modSettings', null, 90);
501
502
		return;
503
	}
504
505
	$replaceArray = array();
506
	foreach ($changeArray as $variable => $value)
507
	{
508
		// Don't bother if it's already like that ;).
509
		if (isset($modSettings[$variable]) && $modSettings[$variable] == $value)
510
			continue;
511
		// If the variable isn't set, but would only be set to nothing'ness, then don't bother setting it.
512
		elseif (!isset($modSettings[$variable]) && empty($value))
513
			continue;
514
515
		$replaceArray[] = array($variable, $value);
516
517
		$modSettings[$variable] = $value;
518
	}
519
520
	if (empty($replaceArray))
521
		return;
522
523
	$smcFunc['db_insert']('replace',
524
		'{db_prefix}settings',
525
		array('variable' => 'string-255', 'value' => 'string-65534'),
526
		$replaceArray,
527
		array('variable')
528
	);
529
530
	// Kill the cache - it needs redoing now, but we won't bother ourselves with that here.
531
	cache_put_data('modSettings', null, 90);
532
}
533
534
/**
535
 * Constructs a page list.
536
 *
537
 * - builds the page list, e.g. 1 ... 6 7 [8] 9 10 ... 15.
538
 * - flexible_start causes it to use "url.page" instead of "url;start=page".
539
 * - very importantly, cleans up the start value passed, and forces it to
540
 *   be a multiple of num_per_page.
541
 * - checks that start is not more than max_value.
542
 * - base_url should be the URL without any start parameter on it.
543
 * - uses the compactTopicPagesEnable and compactTopicPagesContiguous
544
 *   settings to decide how to display the menu.
545
 *
546
 * an example is available near the function definition.
547
 * $pageindex = constructPageIndex($scripturl . '?board=' . $board, $_REQUEST['start'], $num_messages, $maxindex, true);
548
 *
549
 * @param string $base_url The basic URL to be used for each link.
550
 * @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.
551
 * @param int $max_value The total number of items you are paginating for.
552
 * @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.
553
 * @param bool $flexible_start Whether a ;start=x component should be introduced into the URL automatically (see above)
554
 * @param bool $show_prevnext Whether the Previous and Next links should be shown (should be on only when navigating the list)
555
 *
556
 * @return string The complete HTML of the page index that was requested, formatted by the template.
557
 */
558
function constructPageIndex($base_url, &$start, $max_value, $num_per_page, $flexible_start = false, $show_prevnext = true)
559
{
560
	global $modSettings, $context, $smcFunc, $settings, $txt;
561
562
	// Save whether $start was less than 0 or not.
563
	$start = (int) $start;
564
	$start_invalid = $start < 0;
565
566
	// Make sure $start is a proper variable - not less than 0.
567
	if ($start_invalid)
568
		$start = 0;
569
	// Not greater than the upper bound.
570
	elseif ($start >= $max_value)
571
		$start = max(0, (int) $max_value - (((int) $max_value % (int) $num_per_page) == 0 ? $num_per_page : ((int) $max_value % (int) $num_per_page)));
572
	// And it has to be a multiple of $num_per_page!
573
	else
574
		$start = max(0, (int) $start - ((int) $start % (int) $num_per_page));
575
576
	$context['current_page'] = $start / $num_per_page;
577
578
	// Define some default page index settings if we don't already have it...
579
	if (!isset($settings['page_index']))
580
	{
581
		// This defines the formatting for the page indexes used throughout the forum.
582
		$settings['page_index'] = array(
583
			'extra_before' => '<span class="pages">' . $txt['pages'] . '</span>',
584
			'previous_page' => '<span class="main_icons previous_page"></span>',
585
			'current_page' => '<span class="current_page">%1$d</span> ',
586
			'page' => '<a class="nav_page" href="{URL}">%2$s</a> ',
587
			'expand_pages' => '<span class="expand_pages" onclick="expandPages(this, {LINK}, {FIRST_PAGE}, {LAST_PAGE}, {PER_PAGE});"> ... </span>',
588
			'next_page' => '<span class="main_icons next_page"></span>',
589
			'extra_after' => '',
590
		);
591
	}
592
593
	$base_link = strtr($settings['page_index']['page'], array('{URL}' => $flexible_start ? $base_url : strtr($base_url, array('%' => '%%')) . ';start=%1$d'));
594
	$pageindex = $settings['page_index']['extra_before'];
595
596
	// Compact pages is off or on?
597
	if (empty($modSettings['compactTopicPagesEnable']))
598
	{
599
		// Show the left arrow.
600
		$pageindex .= $start == 0 ? ' ' : sprintf($base_link, $start - $num_per_page, $settings['page_index']['previous_page']);
601
602
		// Show all the pages.
603
		$display_page = 1;
604
		for ($counter = 0; $counter < $max_value; $counter += $num_per_page)
605
			$pageindex .= $start == $counter && !$start_invalid ? sprintf($settings['page_index']['current_page'], $display_page++) : sprintf($base_link, $counter, $display_page++);
606
607
		// Show the right arrow.
608
		$display_page = ($start + $num_per_page) > $max_value ? $max_value : ($start + $num_per_page);
609
		if ($start != $counter - $max_value && !$start_invalid)
610
			$pageindex .= $display_page > $counter - $num_per_page ? ' ' : sprintf($base_link, $display_page, $settings['page_index']['next_page']);
611
	}
612
	else
613
	{
614
		// If they didn't enter an odd value, pretend they did.
615
		$PageContiguous = (int) ($modSettings['compactTopicPagesContiguous'] - ($modSettings['compactTopicPagesContiguous'] % 2)) / 2;
616
617
		// Show the "prev page" link. (>prev page< 1 ... 6 7 [8] 9 10 ... 15 next page)
618
		if (!empty($start) && $show_prevnext)
619
			$pageindex .= sprintf($base_link, $start - $num_per_page, $settings['page_index']['previous_page']);
620
		else
621
			$pageindex .= '';
622
623
		// Show the first page. (prev page >1< ... 6 7 [8] 9 10 ... 15)
624
		if ($start > $num_per_page * $PageContiguous)
625
			$pageindex .= sprintf($base_link, 0, '1');
626
627
		// Show the ... after the first page.  (prev page 1 >...< 6 7 [8] 9 10 ... 15 next page)
628
		if ($start > $num_per_page * ($PageContiguous + 1))
629
			$pageindex .= strtr($settings['page_index']['expand_pages'], array(
630
				'{LINK}' => JavaScriptEscape($smcFunc['htmlspecialchars']($base_link)),
631
				'{FIRST_PAGE}' => $num_per_page,
632
				'{LAST_PAGE}' => $start - $num_per_page * $PageContiguous,
633
				'{PER_PAGE}' => $num_per_page,
634
			));
635
636
		// Show the pages before the current one. (prev page 1 ... >6 7< [8] 9 10 ... 15 next page)
637
		for ($nCont = $PageContiguous; $nCont >= 1; $nCont--)
638
			if ($start >= $num_per_page * $nCont)
639
			{
640
				$tmpStart = $start - $num_per_page * $nCont;
641
				$pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
642
			}
643
644
		// Show the current page. (prev page 1 ... 6 7 >[8]< 9 10 ... 15 next page)
645
		if (!$start_invalid)
646
			$pageindex .= sprintf($settings['page_index']['current_page'], $start / $num_per_page + 1);
647
		else
648
			$pageindex .= sprintf($base_link, $start, $start / $num_per_page + 1);
649
650
		// Show the pages after the current one... (prev page 1 ... 6 7 [8] >9 10< ... 15 next page)
651
		$tmpMaxPages = (int) (($max_value - 1) / $num_per_page) * $num_per_page;
652
		for ($nCont = 1; $nCont <= $PageContiguous; $nCont++)
653
			if ($start + $num_per_page * $nCont <= $tmpMaxPages)
654
			{
655
				$tmpStart = $start + $num_per_page * $nCont;
656
				$pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
657
			}
658
659
		// Show the '...' part near the end. (prev page 1 ... 6 7 [8] 9 10 >...< 15 next page)
660
		if ($start + $num_per_page * ($PageContiguous + 1) < $tmpMaxPages)
661
			$pageindex .= strtr($settings['page_index']['expand_pages'], array(
662
				'{LINK}' => JavaScriptEscape($smcFunc['htmlspecialchars']($base_link)),
663
				'{FIRST_PAGE}' => $start + $num_per_page * ($PageContiguous + 1),
664
				'{LAST_PAGE}' => $tmpMaxPages,
665
				'{PER_PAGE}' => $num_per_page,
666
			));
667
668
		// Show the last number in the list. (prev page 1 ... 6 7 [8] 9 10 ... >15<  next page)
669
		if ($start + $num_per_page * $PageContiguous < $tmpMaxPages)
670
			$pageindex .= sprintf($base_link, $tmpMaxPages, $tmpMaxPages / $num_per_page + 1);
671
672
		// Show the "next page" link. (prev page 1 ... 6 7 [8] 9 10 ... 15 >next page<)
673
		if ($start != $tmpMaxPages && $show_prevnext)
674
			$pageindex .= sprintf($base_link, $start + $num_per_page, $settings['page_index']['next_page']);
675
	}
676
	$pageindex .= $settings['page_index']['extra_after'];
677
678
	return $pageindex;
679
}
680
681
/**
682
 * - Formats a number.
683
 * - uses the format of number_format to decide how to format the number.
684
 *   for example, it might display "1 234,50".
685
 * - caches the formatting data from the setting for optimization.
686
 *
687
 * @param float $number A number
688
 * @param bool|int $override_decimal_count If set, will use the specified number of decimal places. Otherwise it's automatically determined
689
 * @return string A formatted number
690
 */
691
function comma_format($number, $override_decimal_count = false)
692
{
693
	global $txt;
694
	static $thousands_separator = null, $decimal_separator = null, $decimal_count = null;
695
696
	// Cache these values...
697
	if ($decimal_separator === null)
698
	{
699
		// Not set for whatever reason?
700
		if (empty($txt['number_format']) || preg_match('~^1([^\d]*)?234([^\d]*)(0*?)$~', $txt['number_format'], $matches) != 1)
701
			return $number;
702
703
		// Cache these each load...
704
		$thousands_separator = $matches[1];
705
		$decimal_separator = $matches[2];
706
		$decimal_count = strlen($matches[3]);
707
	}
708
709
	// Format the string with our friend, number_format.
710
	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

710
	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...
711
}
712
713
/**
714
 * Format a time to make it look purdy.
715
 *
716
 * - returns a pretty formatted version of time based on the user's format in $user_info['time_format'].
717
 * - applies all necessary time offsets to the timestamp, unless offset_type is set.
718
 * - if todayMod is set and show_today was not not specified or true, an
719
 *   alternate format string is used to show the date with something to show it is "today" or "yesterday".
720
 * - performs localization (more than just strftime would do alone.)
721
 *
722
 * @param int $log_time A timestamp
723
 * @param bool $show_today Whether to show "Today"/"Yesterday" or just a date
724
 * @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.
725
 * @param bool $process_safe Activate setlocale check for changes at runtime. Slower, but safer.
726
 * @return string A formatted timestamp
727
 */
728
function timeformat($log_time, $show_today = true, $offset_type = false, $process_safe = false)
729
{
730
	global $context, $user_info, $txt, $modSettings;
731
	static $non_twelve_hour, $locale, $now;
732
	static $unsupportedFormats, $finalizedFormats;
733
734
	$unsupportedFormatsWindows = array('z', 'Z');
735
736
	// Ensure required values are set
737
	$user_info['time_format'] = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %H:%M');
738
739
	// Offset the time.
740
	if (!$offset_type)
741
		$log_time = forum_time(true, $log_time);
742
	// Just the forum offset?
743
	elseif ($offset_type == 'forum')
744
		$log_time = forum_time(false, $log_time);
745
746
	// We can't have a negative date (on Windows, at least.)
747
	if ($log_time < 0)
748
		$log_time = 0;
749
750
	// Today and Yesterday?
751
	$prefix = '';
752
	if ($modSettings['todayMod'] >= 1 && $show_today === true)
753
	{
754
		$now_time = forum_time();
755
756
		if ($now_time - $log_time < (86400 * $modSettings['todayMod']))
757
		{
758
			$then = @getdate($log_time);
759
			$now = (!empty($now) ? $now : @getdate($now_time));
760
761
			// Same day of the year, same year.... Today!
762
			if ($then['yday'] == $now['yday'] && $then['year'] == $now['year'])
763
			{
764
				$prefix = $txt['today'];
765
			}
766
			// 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...
767
			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))
768
			{
769
				$prefix = $txt['yesterday'];
770
			}
771
		}
772
	}
773
774
	$str = !is_bool($show_today) ? $show_today : $user_info['time_format'];
0 ignored issues
show
introduced by
The condition is_bool($show_today) is always true.
Loading history...
775
776
	// Use the cached formats if available
777
	if (is_null($finalizedFormats))
778
		$finalizedFormats = (array) cache_get_data('timeformatstrings', 86400);
779
780
	if (!isset($finalizedFormats[$str]) || !is_array($finalizedFormats[$str]))
781
		$finalizedFormats[$str] = array();
782
783
	// Make a supported version for this format if we don't already have one
784
	$format_type = !empty($prefix) ? 'time_only' : 'normal';
785
	if (empty($finalizedFormats[$str][$format_type]))
786
	{
787
		$timeformat = $format_type == 'time_only' ? get_date_or_time_format('time', $str) : $str;
788
789
		// Not all systems support all formats, and Windows fails altogether if unsupported ones are
790
		// used, so let's prevent that. Some substitutions go to the nearest reasonable fallback, some
791
		// turn into static strings, some (i.e. %a, %A, %b, %B, %p) have special handling below.
792
		$strftimeFormatSubstitutions = array(
793
			// Day
794
			'a' => '#txt_days_short_%w#', 'A' => '#txt_days_%w#', 'e' => '%d', 'd' => '&#37;d', 'j' => '&#37;j', 'u' => '%w', 'w' => '&#37;w',
795
			// Week
796
			'U' => '&#37;U', 'V' => '%U', 'W' => '%U',
797
			// Month
798
			'b' => '#txt_months_short_%m#', 'B' => '#txt_months_%m#', 'h' => '%b', 'm' => '&#37;m',
799
			// Year
800
			'C' => '&#37;C', 'g' => '%y', 'G' => '%Y', 'y' => '&#37;y', 'Y' => '&#37;Y',
801
			// Time
802
			'H' => '&#37;H', 'k' => '%H', 'I' => '%H', 'l' => '%I', 'M' => '&#37;M', 'p' => '&#37;p', 'P' => '%p',
803
			'r' => '%I:%M:%S %p', 'R' => '%H:%M', 'S' => '&#37;S', 'T' => '%H:%M:%S', 'X' => '%T', 'z' => '&#37;z', 'Z' => '&#37;Z',
804
			// Time and Date Stamps
805
			'c' => '%F %T', 'D' => '%m/%d/%y', 'F' => '%Y-%m-%d', 's' => '&#37;s', 'x' => '%F',
806
			// Miscellaneous
807
			'n' => "\n", 't' => "\t", '%' => '&#37;',
808
		);
809
810
		// No need to do this part again if we already did it once
811
		if (is_null($unsupportedFormats))
812
			$unsupportedFormats = (array) cache_get_data('unsupportedtimeformats', 86400);
813
		if (empty($unsupportedFormats))
814
		{
815
			foreach ($strftimeFormatSubstitutions as $format => $substitution)
816
			{
817
				// Avoid a crashing bug with PHP 7 on certain versions of Windows
818
				if ($context['server']['is_windows'] && in_array($format, $unsupportedFormatsWindows))
819
				{
820
					$unsupportedFormats[] = $format;
821
					continue;
822
				}
823
824
				$value = @strftime('%' . $format);
825
826
				// Windows will return false for unsupported formats
827
				// Other operating systems return the format string as a literal
828
				if ($value === false || $value === $format)
829
					$unsupportedFormats[] = $format;
830
			}
831
			cache_put_data('unsupportedtimeformats', $unsupportedFormats, 86400);
832
		}
833
834
		// Windows needs extra help if $timeformat contains something completely invalid, e.g. '%Q'
835
		if (DIRECTORY_SEPARATOR === '\\')
836
			$timeformat = preg_replace('~%(?!' . implode('|', array_keys($strftimeFormatSubstitutions)) . ')~', '&#37;', $timeformat);
837
838
		// Substitute unsupported formats with supported ones
839
		if (!empty($unsupportedFormats))
840
			while (preg_match('~%(' . implode('|', $unsupportedFormats) . ')~', $timeformat, $matches))
841
				$timeformat = str_replace($matches[0], $strftimeFormatSubstitutions[$matches[1]], $timeformat);
842
843
		// Remember this so we don't need to do it again
844
		$finalizedFormats[$str][$format_type] = $timeformat;
845
		cache_put_data('timeformatstrings', $finalizedFormats, 86400);
846
	}
847
848
	$timeformat = $finalizedFormats[$str][$format_type];
849
850
	// Make sure we are using the correct locale.
851
	if (!isset($locale) || ($process_safe === true && setlocale(LC_TIME, '0') != $locale))
852
		$locale = setlocale(LC_TIME, array($txt['lang_locale'] . '.' . $modSettings['global_character_set'], $txt['lang_locale'] . '.' . $txt['lang_character_set'], $txt['lang_locale']));
853
854
	// If the current locale is unsupported, we'll have to localize the hard way.
855
	if ($locale === false)
856
	{
857
		$timeformat = strtr($timeformat, array(
858
			'%a' => '#txt_days_short_%w#',
859
			'%A' => '#txt_days_%w#',
860
			'%b' => '#txt_months_short_%m#',
861
			'%B' => '#txt_months_%m#',
862
			'%p' => '&#37;p',
863
			'%P' => '&#37;p'
864
		));
865
	}
866
	// Just in case the locale doesn't support '%p' properly.
867
	// @todo Is this even necessary?
868
	else
869
	{
870
		if (!isset($non_twelve_hour) && strpos($timeformat, '%p') !== false)
871
			$non_twelve_hour = trim(strftime('%p')) === '';
872
873
		if (!empty($non_twelve_hour))
874
			$timeformat = strtr($timeformat, array(
875
				'%p' => '&#37;p',
876
				'%P' => '&#37;p'
877
			));
878
	}
879
880
	// And now, the moment we've all be waiting for...
881
	$timestring = strftime($timeformat, $log_time);
882
883
	// Do-it-yourself time localization.  Fun.
884
	if (strpos($timestring, '&#37;p') !== false)
885
		$timestring = str_replace('&#37;p', (strftime('%H', $log_time) < 12 ? $txt['time_am'] : $txt['time_pm']), $timestring);
886
	if (strpos($timestring, '#txt_') !== false)
887
	{
888
		if (strpos($timestring, '#txt_days_short_') !== false)
889
			$timestring = strtr($timestring, array(
890
				'#txt_days_short_0#' => $txt['days_short'][0],
891
				'#txt_days_short_1#' => $txt['days_short'][1],
892
				'#txt_days_short_2#' => $txt['days_short'][2],
893
				'#txt_days_short_3#' => $txt['days_short'][3],
894
				'#txt_days_short_4#' => $txt['days_short'][4],
895
				'#txt_days_short_5#' => $txt['days_short'][5],
896
				'#txt_days_short_6#' => $txt['days_short'][6],
897
			));
898
899
		if (strpos($timestring, '#txt_days_') !== false)
900
			$timestring = strtr($timestring, array(
901
				'#txt_days_0#' => $txt['days'][0],
902
				'#txt_days_1#' => $txt['days'][1],
903
				'#txt_days_2#' => $txt['days'][2],
904
				'#txt_days_3#' => $txt['days'][3],
905
				'#txt_days_4#' => $txt['days'][4],
906
				'#txt_days_5#' => $txt['days'][5],
907
				'#txt_days_6#' => $txt['days'][6],
908
			));
909
910
		if (strpos($timestring, '#txt_months_short_') !== false)
911
			$timestring = strtr($timestring, array(
912
				'#txt_months_short_01#' => $txt['months_short'][1],
913
				'#txt_months_short_02#' => $txt['months_short'][2],
914
				'#txt_months_short_03#' => $txt['months_short'][3],
915
				'#txt_months_short_04#' => $txt['months_short'][4],
916
				'#txt_months_short_05#' => $txt['months_short'][5],
917
				'#txt_months_short_06#' => $txt['months_short'][6],
918
				'#txt_months_short_07#' => $txt['months_short'][7],
919
				'#txt_months_short_08#' => $txt['months_short'][8],
920
				'#txt_months_short_09#' => $txt['months_short'][9],
921
				'#txt_months_short_10#' => $txt['months_short'][10],
922
				'#txt_months_short_11#' => $txt['months_short'][11],
923
				'#txt_months_short_12#' => $txt['months_short'][12],
924
			));
925
926
		if (strpos($timestring, '#txt_months_') !== false)
927
			$timestring = strtr($timestring, array(
928
				'#txt_months_01#' => $txt['months'][1],
929
				'#txt_months_02#' => $txt['months'][2],
930
				'#txt_months_03#' => $txt['months'][3],
931
				'#txt_months_04#' => $txt['months'][4],
932
				'#txt_months_05#' => $txt['months'][5],
933
				'#txt_months_06#' => $txt['months'][6],
934
				'#txt_months_07#' => $txt['months'][7],
935
				'#txt_months_08#' => $txt['months'][8],
936
				'#txt_months_09#' => $txt['months'][9],
937
				'#txt_months_10#' => $txt['months'][10],
938
				'#txt_months_11#' => $txt['months'][11],
939
				'#txt_months_12#' => $txt['months'][12],
940
			));
941
	}
942
943
	// Restore any literal percent characters, add the prefix, and we're done.
944
	return $prefix . str_replace('&#37;', '%', $timestring);
945
}
946
947
/**
948
 * Gets a version of a strftime() format that only shows the date or time components
949
 *
950
 * @param string $type Either 'date' or 'time'.
951
 * @param string $format A strftime() format to process. Defaults to $user_info['time_format'].
952
 * @return string A strftime() format string
953
 */
954
function get_date_or_time_format($type = '', $format = '')
955
{
956
	global $user_info, $modSettings;
957
	static $formats;
958
959
	// If the format is invalid, fall back to defaults.
960
	if (strpos($format, '%') === false)
961
		$format = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %k:%M');
962
963
	$orig_format = $format;
964
965
	// Have we already done this?
966
	if (isset($formats[$orig_format][$type]))
967
		return $formats[$orig_format][$type];
968
969
	if ($type === 'date')
970
	{
971
		$specifications = array(
972
			// Day
973
			'%a' => '%a', '%A' => '%A', '%e' => '%e', '%d' => '%d', '%j' => '%j', '%u' => '%u', '%w' => '%w',
974
			// Week
975
			'%U' => '%U', '%V' => '%V', '%W' => '%W',
976
			// Month
977
			'%b' => '%b', '%B' => '%B', '%h' => '%h', '%m' => '%m',
978
			// Year
979
			'%C' => '%C', '%g' => '%g', '%G' => '%G', '%y' => '%y', '%Y' => '%Y',
980
			// Time
981
			'%H' => '', '%k' => '', '%I' => '', '%l' => '', '%M' => '', '%p' => '', '%P' => '',
982
			'%r' => '', '%R' => '', '%S' => '', '%T' => '', '%X' => '', '%z' => '', '%Z' => '',
983
			// Time and Date Stamps
984
			'%c' => '%x', '%D' => '%D', '%F' => '%F', '%s' => '%s', '%x' => '%x',
985
			// Miscellaneous
986
			'%n' => '', '%t' => '', '%%' => '%%',
987
		);
988
989
		$default_format = '%F';
990
	}
991
	elseif ($type === 'time')
992
	{
993
		$specifications = array(
994
			// Day
995
			'%a' => '', '%A' => '', '%e' => '', '%d' => '', '%j' => '', '%u' => '', '%w' => '',
996
			// Week
997
			'%U' => '', '%V' => '', '%W' => '',
998
			// Month
999
			'%b' => '', '%B' => '', '%h' => '', '%m' => '',
1000
			// Year
1001
			'%C' => '', '%g' => '', '%G' => '', '%y' => '', '%Y' => '',
1002
			// Time
1003
			'%H' => '%H', '%k' => '%k', '%I' => '%I', '%l' => '%l', '%M' => '%M', '%p' => '%p', '%P' => '%P',
1004
			'%r' => '%r', '%R' => '%R', '%S' => '%S', '%T' => '%T', '%X' => '%X', '%z' => '%z', '%Z' => '%Z',
1005
			// Time and Date Stamps
1006
			'%c' => '%X', '%D' => '', '%F' => '', '%s' => '%s', '%x' => '',
1007
			// Miscellaneous
1008
			'%n' => '', '%t' => '', '%%' => '%%',
1009
		);
1010
1011
		$default_format = '%k:%M';
1012
	}
1013
	// Invalid type requests just get the full format string.
1014
	else
1015
		return $format;
1016
1017
	// Separate the specifications we want from the ones we don't.
1018
	$wanted = array_filter($specifications);
1019
	$unwanted = array_diff(array_keys($specifications), $wanted);
1020
1021
	// First, make any necessary substitutions in the format.
1022
	$format = strtr($format, $wanted);
1023
1024
	// Next, strip out any specifications and literal text that we don't want.
1025
	$format_parts = preg_split('~%[' . (strtr(implode('', $unwanted), array('%' => ''))) . ']~u', $format);
1026
1027
	foreach ($format_parts as $p => $f)
1028
	{
1029
		if (strpos($f, '%') === false)
1030
			unset($format_parts[$p]);
1031
	}
1032
1033
	$format = implode('', $format_parts);
0 ignored issues
show
Bug introduced by
It seems like $format_parts can also be of type false; however, parameter $pieces of implode() does only seem to accept array, 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

1033
	$format = implode('', /** @scrutinizer ignore-type */ $format_parts);
Loading history...
1034
1035
	// Finally, strip out any unwanted leftovers.
1036
	// 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
1037
	$format = preg_replace(
1038
		array(
1039
			// Anything that isn't a specification, punctuation mark, or whitespace.
1040
			'~(?<!%)\p{L}|[^\p{L}\p{P}\s]~u',
1041
			// A series of punctuation marks (except %), possibly separated by whitespace.
1042
			'~([^%\P{P}])(\s*)(?'.'>(\1|[^%\P{Po}])\s*(?!$))*~u',
1043
			// Unwanted trailing punctuation and whitespace.
1044
			'~(?'.'>([\p{Pd}\p{Ps}\p{Pi}\p{Pc}]|[^%\P{Po}])\s*)*$~u',
1045
			// Unwanted opening punctuation and whitespace.
1046
			'~^\s*(?'.'>([\p{Pd}\p{Pe}\p{Pf}\p{Pc}]|[^%\P{Po}])\s*)*~u',
1047
		),
1048
		array(
1049
			'',
1050
			'$1$2',
1051
			'',
1052
			'',
1053
		),
1054
		$format
1055
	);
1056
1057
	// Gotta have something...
1058
	if (empty($format))
1059
		$format = $default_format;
1060
1061
	// Remember what we've done.
1062
	$formats[$orig_format][$type] = trim($format);
1063
1064
	return $formats[$orig_format][$type];
1065
}
1066
1067
/**
1068
 * Replaces special entities in strings with the real characters.
1069
 *
1070
 * Functionally equivalent to htmlspecialchars_decode(), except that this also
1071
 * replaces '&nbsp;' with a simple space character.
1072
 *
1073
 * @param string $string A string
1074
 * @return string The string without entities
1075
 */
1076
function un_htmlspecialchars($string)
1077
{
1078
	global $context;
1079
	static $translation = array();
1080
1081
	// Determine the character set... Default to UTF-8
1082
	if (empty($context['character_set']))
1083
		$charset = 'UTF-8';
1084
	// Use ISO-8859-1 in place of non-supported ISO-8859 charsets...
1085
	elseif (strpos($context['character_set'], 'ISO-8859-') !== false && !in_array($context['character_set'], array('ISO-8859-5', 'ISO-8859-15')))
1086
		$charset = 'ISO-8859-1';
1087
	else
1088
		$charset = $context['character_set'];
1089
1090
	if (empty($translation))
1091
		$translation = array_flip(get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES, $charset)) + array('&#039;' => '\'', '&#39;' => '\'', '&nbsp;' => ' ');
1092
1093
	return strtr($string, $translation);
1094
}
1095
1096
/**
1097
 * Shorten a subject + internationalization concerns.
1098
 *
1099
 * - shortens a subject so that it is either shorter than length, or that length plus an ellipsis.
1100
 * - respects internationalization characters and entities as one character.
1101
 * - avoids trailing entities.
1102
 * - returns the shortened string.
1103
 *
1104
 * @param string $subject The subject
1105
 * @param int $len How many characters to limit it to
1106
 * @return string The shortened subject - either the entire subject (if it's <= $len) or the subject shortened to $len characters with "..." appended
1107
 */
1108
function shorten_subject($subject, $len)
1109
{
1110
	global $smcFunc;
1111
1112
	// It was already short enough!
1113
	if ($smcFunc['strlen']($subject) <= $len)
1114
		return $subject;
1115
1116
	// Shorten it by the length it was too long, and strip off junk from the end.
1117
	return $smcFunc['substr']($subject, 0, $len) . '...';
1118
}
1119
1120
/**
1121
 * Gets the current time with offset.
1122
 *
1123
 * - always applies the offset in the time_offset setting.
1124
 *
1125
 * @param bool $use_user_offset Whether to apply the user's offset as well
1126
 * @param int $timestamp A timestamp (null to use current time)
1127
 * @return int Seconds since the unix epoch, with forum time offset and (optionally) user time offset applied
1128
 */
1129
function forum_time($use_user_offset = true, $timestamp = null)
1130
{
1131
	global $user_info, $modSettings;
1132
1133
	// Ensure required values are set
1134
	$modSettings['time_offset'] = !empty($modSettings['time_offset']) ? $modSettings['time_offset'] : 0;
1135
	$user_info['time_offset'] = !empty($user_info['time_offset']) ? $user_info['time_offset'] : 0;
1136
1137
	if ($timestamp === null)
1138
		$timestamp = time();
1139
	elseif ($timestamp == 0)
1140
		return 0;
1141
1142
	return $timestamp + ($modSettings['time_offset'] + ($use_user_offset ? $user_info['time_offset'] : 0)) * 3600;
1143
}
1144
1145
/**
1146
 * Calculates all the possible permutations (orders) of array.
1147
 * should not be called on huge arrays (bigger than like 10 elements.)
1148
 * returns an array containing each permutation.
1149
 *
1150
 * @deprecated since 2.1
1151
 * @param array $array An array
1152
 * @return array An array containing each permutation
1153
 */
1154
function permute($array)
1155
{
1156
	$orders = array($array);
1157
1158
	$n = count($array);
1159
	$p = range(0, $n);
1160
	for ($i = 1; $i < $n; null)
1161
	{
1162
		$p[$i]--;
1163
		$j = $i % 2 != 0 ? $p[$i] : 0;
1164
1165
		$temp = $array[$i];
1166
		$array[$i] = $array[$j];
1167
		$array[$j] = $temp;
1168
1169
		for ($i = 1; $p[$i] == 0; $i++)
1170
			$p[$i] = 1;
1171
1172
		$orders[] = $array;
1173
	}
1174
1175
	return $orders;
1176
}
1177
1178
/**
1179
 * Parse bulletin board code in a string, as well as smileys optionally.
1180
 *
1181
 * - only parses bbc tags which are not disabled in disabledBBC.
1182
 * - handles basic HTML, if enablePostHTML is on.
1183
 * - caches the from/to replace regular expressions so as not to reload them every time a string is parsed.
1184
 * - only parses smileys if smileys is true.
1185
 * - does nothing if the enableBBC setting is off.
1186
 * - uses the cache_id as a unique identifier to facilitate any caching it may do.
1187
 * - returns the modified message.
1188
 *
1189
 * @param string|bool $message The message.
1190
 *		When a empty string, nothing is done.
1191
 *		When false we provide a list of BBC codes available.
1192
 *		When a string, the message is parsed and bbc handled.
1193
 * @param bool $smileys Whether to parse smileys as well
1194
 * @param string $cache_id The cache ID
1195
 * @param array $parse_tags If set, only parses these tags rather than all of them
1196
 * @return string The parsed message
1197
 */
1198
function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = array())
1199
{
1200
	global $smcFunc, $txt, $scripturl, $context, $modSettings, $user_info, $sourcedir, $cache_enable;
1201
	static $bbc_lang_locales = array(), $itemcodes = array(), $no_autolink_tags = array();
1202
	static $disabled, $alltags_regex = '', $param_regexes = array();
1203
1204
	// Don't waste cycles
1205
	if ($message === '')
1206
		return '';
1207
1208
	// Just in case it wasn't determined yet whether UTF-8 is enabled.
1209
	if (!isset($context['utf8']))
1210
		$context['utf8'] = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8';
1211
1212
	// Clean up any cut/paste issues we may have
1213
	$message = sanitizeMSCutPaste($message);
1214
1215
	// If the load average is too high, don't parse the BBC.
1216
	if (!empty($context['load_average']) && !empty($modSettings['bbc']) && $context['load_average'] >= $modSettings['bbc'])
1217
	{
1218
		$context['disabled_parse_bbc'] = true;
1219
		return $message;
1220
	}
1221
1222
	if ($smileys !== null && ($smileys == '1' || $smileys == '0'))
1223
		$smileys = (bool) $smileys;
1224
1225
	if (empty($modSettings['enableBBC']) && $message !== false)
1226
	{
1227
		if ($smileys === true)
1228
			parsesmileys($message);
1229
1230
		return $message;
1231
	}
1232
1233
	// If we already have a version of the BBCodes for the current language, use that. Otherwise, make one.
1234
	if (!empty($bbc_lang_locales[$txt['lang_locale']]))
1235
		$bbc_codes = $bbc_lang_locales[$txt['lang_locale']];
1236
	else
1237
		$bbc_codes = array();
1238
1239
	// If we are not doing every tag then we don't cache this run.
1240
	if (!empty($parse_tags))
1241
		$bbc_codes = array();
1242
1243
	// Ensure $modSettings['tld_regex'] contains a valid regex for the autolinker
1244
	if (!empty($modSettings['autoLinkUrls']))
1245
		set_tld_regex();
1246
1247
	// Allow mods access before entering the main parse_bbc loop
1248
	call_integration_hook('integrate_pre_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
1249
1250
	// Sift out the bbc for a performance improvement.
1251
	if (empty($bbc_codes) || $message === false || !empty($parse_tags))
1252
	{
1253
		if (!empty($modSettings['disabledBBC']))
1254
		{
1255
			$disabled = array();
1256
1257
			$temp = explode(',', strtolower($modSettings['disabledBBC']));
1258
1259
			foreach ($temp as $tag)
1260
				$disabled[trim($tag)] = true;
1261
1262
			if (in_array('color', $disabled))
1263
				$disabled = array_merge($disabled, array(
1264
					'black' => true,
1265
					'white' => true,
1266
					'red' => true,
1267
					'green' => true,
1268
					'blue' => true,
1269
					)
1270
				);
1271
		}
1272
1273
		// The YouTube bbc needs this for its origin parameter
1274
		$scripturl_parts = parse_url($scripturl);
1275
		$hosturl = $scripturl_parts['scheme'] . '://' . $scripturl_parts['host'];
1276
1277
		/* The following bbc are formatted as an array, with keys as follows:
1278
1279
			tag: the tag's name - should be lowercase!
1280
1281
			type: one of...
1282
				- (missing): [tag]parsed content[/tag]
1283
				- unparsed_equals: [tag=xyz]parsed content[/tag]
1284
				- parsed_equals: [tag=parsed data]parsed content[/tag]
1285
				- unparsed_content: [tag]unparsed content[/tag]
1286
				- closed: [tag], [tag/], [tag /]
1287
				- unparsed_commas: [tag=1,2,3]parsed content[/tag]
1288
				- unparsed_commas_content: [tag=1,2,3]unparsed content[/tag]
1289
				- unparsed_equals_content: [tag=...]unparsed content[/tag]
1290
1291
			parameters: an optional array of parameters, for the form
1292
			  [tag abc=123]content[/tag].  The array is an associative array
1293
			  where the keys are the parameter names, and the values are an
1294
			  array which may contain the following:
1295
				- match: a regular expression to validate and match the value.
1296
				- quoted: true if the value should be quoted.
1297
				- validate: callback to evaluate on the data, which is $data.
1298
				- value: a string in which to replace $1 with the data.
1299
					Either value or validate may be used, not both.
1300
				- optional: true if the parameter is optional.
1301
				- default: a default value for missing optional parameters.
1302
1303
			test: a regular expression to test immediately after the tag's
1304
			  '=', ' ' or ']'.  Typically, should have a \] at the end.
1305
			  Optional.
1306
1307
			content: only available for unparsed_content, closed,
1308
			  unparsed_commas_content, and unparsed_equals_content.
1309
			  $1 is replaced with the content of the tag.  Parameters
1310
			  are replaced in the form {param}.  For unparsed_commas_content,
1311
			  $2, $3, ..., $n are replaced.
1312
1313
			before: only when content is not used, to go before any
1314
			  content.  For unparsed_equals, $1 is replaced with the value.
1315
			  For unparsed_commas, $1, $2, ..., $n are replaced.
1316
1317
			after: similar to before in every way, except that it is used
1318
			  when the tag is closed.
1319
1320
			disabled_content: used in place of content when the tag is
1321
			  disabled.  For closed, default is '', otherwise it is '$1' if
1322
			  block_level is false, '<div>$1</div>' elsewise.
1323
1324
			disabled_before: used in place of before when disabled.  Defaults
1325
			  to '<div>' if block_level, '' if not.
1326
1327
			disabled_after: used in place of after when disabled.  Defaults
1328
			  to '</div>' if block_level, '' if not.
1329
1330
			block_level: set to true the tag is a "block level" tag, similar
1331
			  to HTML.  Block level tags cannot be nested inside tags that are
1332
			  not block level, and will not be implicitly closed as easily.
1333
			  One break following a block level tag may also be removed.
1334
1335
			trim: if set, and 'inside' whitespace after the begin tag will be
1336
			  removed.  If set to 'outside', whitespace after the end tag will
1337
			  meet the same fate.
1338
1339
			validate: except when type is missing or 'closed', a callback to
1340
			  validate the data as $data.  Depending on the tag's type, $data
1341
			  may be a string or an array of strings (corresponding to the
1342
			  replacement.)
1343
1344
			quoted: when type is 'unparsed_equals' or 'parsed_equals' only,
1345
			  may be not set, 'optional', or 'required' corresponding to if
1346
			  the content may be quoted.  This allows the parser to read
1347
			  [tag="abc]def[esdf]"] properly.
1348
1349
			require_parents: an array of tag names, or not set.  If set, the
1350
			  enclosing tag *must* be one of the listed tags, or parsing won't
1351
			  occur.
1352
1353
			require_children: similar to require_parents, if set children
1354
			  won't be parsed if they are not in the list.
1355
1356
			disallow_children: similar to, but very different from,
1357
			  require_children, if it is set the listed tags will not be
1358
			  parsed inside the tag.
1359
1360
			parsed_tags_allowed: an array restricting what BBC can be in the
1361
			  parsed_equals parameter, if desired.
1362
		*/
1363
1364
		$codes = array(
1365
			array(
1366
				'tag' => 'abbr',
1367
				'type' => 'unparsed_equals',
1368
				'before' => '<abbr title="$1">',
1369
				'after' => '</abbr>',
1370
				'quoted' => 'optional',
1371
				'disabled_after' => ' ($1)',
1372
			),
1373
			// Legacy (and just an alias for [abbr] even when enabled)
1374
			array(
1375
				'tag' => 'acronym',
1376
				'type' => 'unparsed_equals',
1377
				'before' => '<abbr title="$1">',
1378
				'after' => '</abbr>',
1379
				'quoted' => 'optional',
1380
				'disabled_after' => ' ($1)',
1381
			),
1382
			array(
1383
				'tag' => 'anchor',
1384
				'type' => 'unparsed_equals',
1385
				'test' => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]',
1386
				'before' => '<span id="post_$1">',
1387
				'after' => '</span>',
1388
			),
1389
			array(
1390
				'tag' => 'attach',
1391
				'type' => 'unparsed_content',
1392
				'parameters' => array(
1393
					'id' => array('match' => '(\d+)'),
1394
					'alt' => array('optional' => true),
1395
					'width' => array('optional' => true, 'match' => '(\d+)'),
1396
					'height' => array('optional' => true, 'match' => '(\d+)'),
1397
					'display' => array('optional' => true, 'match' => '(link|embed)'),
1398
				),
1399
				'content' => '$1',
1400
				'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...
1401
				{
1402
					$returnContext = '';
1403
1404
					// BBC or the entire attachments feature is disabled
1405
					if (empty($modSettings['attachmentEnable']) || !empty($disabled['attach']))
1406
						return $data;
1407
1408
					// Save the attach ID.
1409
					$attachID = $params['{id}'];
1410
1411
					// Kinda need this.
1412
					require_once($sourcedir . '/Subs-Attachments.php');
1413
1414
					$currentAttachment = parseAttachBBC($attachID);
1415
1416
					// parseAttachBBC will return a string ($txt key) rather than dying with a fatal_error. Up to you to decide what to do.
1417
					if (is_string($currentAttachment))
1418
						return $data = !empty($txt[$currentAttachment]) ? $txt[$currentAttachment] : $currentAttachment;
1419
1420
					// We need a display mode.
1421
					if (empty($params['{display}']))
1422
					{
1423
						// Images, video, and audio are embedded by default.
1424
						if (!empty($currentAttachment['is_image']) || strpos($currentAttachment['mime_type'], 'video/') === 0 || strpos($currentAttachment['mime_type'], 'audio/') === 0)
1425
							$params['{display}'] = 'embed';
1426
						// Anything else shows a link by default.
1427
						else
1428
							$params['{display}'] = 'link';
1429
					}
1430
1431
					// Embedded file.
1432
					if ($params['{display}'] == 'embed')
1433
					{
1434
						$alt = ' alt="' . (!empty($params['{alt}']) ? $params['{alt}'] : $currentAttachment['name']) . '"';
1435
						$title = !empty($data) ? ' title="' . $smcFunc['htmlspecialchars']($data) . '"' : '';
1436
1437
						if (empty($params['{width}']) && empty($params['{height}']))
1438
						{
1439
							$width = !empty($currentAttachment['width']) ? $currentAttachment['width'] : '';
1440
							$height = !empty($currentAttachment['height']) ? $currentAttachment['height'] : '';
1441
						}
1442
						else
1443
						{
1444
							$width = !empty($params['{width}']) ? $params['{width}'] : '';
1445
							$height = !empty($params['{height}']) ? $params['{height}'] : '';
1446
						}
1447
1448
						// Image.
1449
						if (!empty($currentAttachment['is_image']))
1450
						{
1451
							$width = !empty($width) ? ' width="' . $width . '"' : '';
1452
							$height = !empty($height) ? ' height="' . $height . '"' : '';
1453
1454
							if ($currentAttachment['thumbnail']['has_thumb'] && empty($params['{width}']) && empty($params['{height}']))
1455
								$returnContext .= '<a href="' . $currentAttachment['href'] . ';image" id="link_' . $currentAttachment['id'] . '" onclick="' . $currentAttachment['thumbnail']['javascript'] . '"><img src="' . $currentAttachment['thumbnail']['href'] . '"' . $alt . $title . ' id="thumb_' . $currentAttachment['id'] . '" class="atc_img"></a>';
1456
							else
1457
								$returnContext .= '<img src="' . $currentAttachment['href'] . ';image"' . $alt . $title . $width . $height . ' class="bbc_img"/>';
1458
						}
1459
						// Video.
1460
						elseif (strpos($currentAttachment['mime_type'], 'video/') === 0)
1461
						{
1462
							$width = !empty($width) ? ' width="' . $width . '"' : '';
1463
							$height = !empty($height) ? ' height="' . $height . '"' : '';
1464
1465
							$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>' : '');
1466
						}
1467
						// Audio.
1468
						elseif (strpos($currentAttachment['mime_type'], 'audio/') === 0)
1469
						{
1470
							$width = 'max-width:100%; width: ' . (!empty($width) ? $width : '400') . 'px;';
1471
							$height = !empty($height) ? 'height: ' . $height . 'px;' : '';
1472
1473
							$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>';
1474
						}
1475
						// Anything else.
1476
						else
1477
						{
1478
							$width = !empty($width) ? ' width="' . $width . '"' : '';
1479
							$height = !empty($height) ? ' height="' . $height . '"' : '';
1480
1481
							$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>';
1482
						}
1483
					}
1484
1485
					// No image. Show a link.
1486
					else
1487
						$returnContext .= '<a href="' . $currentAttachment['href'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . '</a>';
1488
1489
					// Use this hook to adjust the HTML output of the attach BBCode.
1490
					// If you want to work with the attachment data itself, use one of these:
1491
					// - integrate_pre_parseAttachBBC
1492
					// - integrate_post_parseAttachBBC
1493
					call_integration_hook('integrate_attach_bbc_validate', array(&$returnContext, $currentAttachment, $tag, $data, $disabled, $params));
1494
1495
					// Gotta append what we just did.
1496
					$data = $returnContext;
1497
				},
1498
			),
1499
			array(
1500
				'tag' => 'b',
1501
				'before' => '<b>',
1502
				'after' => '</b>',
1503
			),
1504
			// Legacy (equivalent to [ltr] or [rtl])
1505
			array(
1506
				'tag' => 'bdo',
1507
				'type' => 'unparsed_equals',
1508
				'before' => '<bdo dir="$1">',
1509
				'after' => '</bdo>',
1510
				'test' => '(rtl|ltr)\]',
1511
				'block_level' => true,
1512
			),
1513
			// Legacy (alias of [color=black])
1514
			array(
1515
				'tag' => 'black',
1516
				'before' => '<span style="color: black;" class="bbc_color">',
1517
				'after' => '</span>',
1518
			),
1519
			// Legacy (alias of [color=blue])
1520
			array(
1521
				'tag' => 'blue',
1522
				'before' => '<span style="color: blue;" class="bbc_color">',
1523
				'after' => '</span>',
1524
			),
1525
			array(
1526
				'tag' => 'br',
1527
				'type' => 'closed',
1528
				'content' => '<br>',
1529
			),
1530
			array(
1531
				'tag' => 'center',
1532
				'before' => '<div class="centertext">',
1533
				'after' => '</div>',
1534
				'block_level' => true,
1535
			),
1536
			array(
1537
				'tag' => 'code',
1538
				'type' => 'unparsed_content',
1539
				'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>',
1540
				// @todo Maybe this can be simplified?
1541
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1542
				{
1543
					if (!isset($disabled['code']))
1544
					{
1545
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
1546
1547
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
0 ignored issues
show
Bug introduced by
It seems like $php_parts can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, 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

1547
						for ($php_i = 0, $php_n = count(/** @scrutinizer ignore-type */ $php_parts); $php_i < $php_n; $php_i++)
Loading history...
1548
						{
1549
							// Do PHP code coloring?
1550
							if ($php_parts[$php_i] != '&lt;?php')
1551
								continue;
1552
1553
							$php_string = '';
1554
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1555
							{
1556
								$php_string .= $php_parts[$php_i];
1557
								$php_parts[$php_i++] = '';
1558
							}
1559
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1560
						}
1561
1562
						// Fix the PHP code stuff...
1563
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
0 ignored issues
show
Bug introduced by
It seems like $php_parts can also be of type false; however, parameter $pieces of implode() does only seem to accept array, 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

1563
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', /** @scrutinizer ignore-type */ $php_parts));
Loading history...
1564
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1565
1566
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1567
						if (!empty($context['browser']['is_opera']))
1568
							$data .= '&nbsp;';
1569
					}
1570
				},
1571
				'block_level' => true,
1572
			),
1573
			array(
1574
				'tag' => 'code',
1575
				'type' => 'unparsed_equals_content',
1576
				'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>',
1577
				// @todo Maybe this can be simplified?
1578
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1579
				{
1580
					if (!isset($disabled['code']))
1581
					{
1582
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1583
1584
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
0 ignored issues
show
Bug introduced by
It seems like $php_parts can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, 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

1584
						for ($php_i = 0, $php_n = count(/** @scrutinizer ignore-type */ $php_parts); $php_i < $php_n; $php_i++)
Loading history...
1585
						{
1586
							// Do PHP code coloring?
1587
							if ($php_parts[$php_i] != '&lt;?php')
1588
								continue;
1589
1590
							$php_string = '';
1591
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1592
							{
1593
								$php_string .= $php_parts[$php_i];
1594
								$php_parts[$php_i++] = '';
1595
							}
1596
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1597
						}
1598
1599
						// Fix the PHP code stuff...
1600
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
0 ignored issues
show
Bug introduced by
It seems like $php_parts can also be of type false; however, parameter $pieces of implode() does only seem to accept array, 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

1600
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', /** @scrutinizer ignore-type */ $php_parts));
Loading history...
1601
						$data[0] = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data[0]);
1602
1603
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1604
						if (!empty($context['browser']['is_opera']))
1605
							$data[0] .= '&nbsp;';
1606
					}
1607
				},
1608
				'block_level' => true,
1609
			),
1610
			array(
1611
				'tag' => 'color',
1612
				'type' => 'unparsed_equals',
1613
				'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]?)\))\]',
1614
				'before' => '<span style="color: $1;" class="bbc_color">',
1615
				'after' => '</span>',
1616
			),
1617
			array(
1618
				'tag' => 'email',
1619
				'type' => 'unparsed_content',
1620
				'content' => '<a href="mailto:$1" class="bbc_email">$1</a>',
1621
				// @todo Should this respect guest_hideContacts?
1622
				'validate' => function(&$tag, &$data, $disabled)
1623
				{
1624
					$data = strtr($data, array('<br>' => ''));
1625
				},
1626
			),
1627
			array(
1628
				'tag' => 'email',
1629
				'type' => 'unparsed_equals',
1630
				'before' => '<a href="mailto:$1" class="bbc_email">',
1631
				'after' => '</a>',
1632
				// @todo Should this respect guest_hideContacts?
1633
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1634
				'disabled_after' => ' ($1)',
1635
			),
1636
			// Legacy (and just a link even when not disabled)
1637
			array(
1638
				'tag' => 'flash',
1639
				'type' => 'unparsed_commas_content',
1640
				'test' => '\d+,\d+\]',
1641
				'content' => '<a href="$1" target="_blank" rel="noopener">$1</a>',
1642
				'validate' => function (&$tag, &$data, $disabled)
1643
				{
1644
					$scheme = parse_url($data[0], PHP_URL_SCHEME);
1645
					if (empty($scheme))
1646
						$data[0] = '//' . ltrim($data[0], ':/');
1647
				},
1648
			),
1649
			array(
1650
				'tag' => 'float',
1651
				'type' => 'unparsed_equals',
1652
				'test' => '(left|right)(\s+max=\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)?\]',
1653
				'before' => '<div $1>',
1654
				'after' => '</div>',
1655
				'validate' => function(&$tag, &$data, $disabled)
1656
				{
1657
					$class = 'class="bbc_float float' . (strpos($data, 'left') === 0 ? 'left' : 'right') . '"';
1658
1659
					if (preg_match('~\bmax=(\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)~', $data, $matches))
1660
						$css = ' style="max-width:' . $matches[1] . (is_numeric($matches[1]) ? 'px' : '') . '"';
1661
					else
1662
						$css = '';
1663
1664
					$data = $class . $css;
1665
				},
1666
				'trim' => 'outside',
1667
				'block_level' => true,
1668
			),
1669
			// Legacy (alias of [url] with an FTP URL)
1670
			array(
1671
				'tag' => 'ftp',
1672
				'type' => 'unparsed_content',
1673
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
1674
				'validate' => function(&$tag, &$data, $disabled)
1675
				{
1676
					$data = strtr($data, array('<br>' => ''));
1677
					$scheme = parse_url($data, PHP_URL_SCHEME);
1678
					if (empty($scheme))
1679
						$data = 'ftp://' . ltrim($data, ':/');
1680
				},
1681
			),
1682
			// Legacy (alias of [url] with an FTP URL)
1683
			array(
1684
				'tag' => 'ftp',
1685
				'type' => 'unparsed_equals',
1686
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
1687
				'after' => '</a>',
1688
				'validate' => function(&$tag, &$data, $disabled)
1689
				{
1690
					$scheme = parse_url($data, PHP_URL_SCHEME);
1691
					if (empty($scheme))
1692
						$data = 'ftp://' . ltrim($data, ':/');
1693
				},
1694
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1695
				'disabled_after' => ' ($1)',
1696
			),
1697
			array(
1698
				'tag' => 'font',
1699
				'type' => 'unparsed_equals',
1700
				'test' => '[A-Za-z0-9_,\-\s]+?\]',
1701
				'before' => '<span style="font-family: $1;" class="bbc_font">',
1702
				'after' => '</span>',
1703
			),
1704
			// Legacy (one of those things that should not be done)
1705
			array(
1706
				'tag' => 'glow',
1707
				'type' => 'unparsed_commas',
1708
				'test' => '[#0-9a-zA-Z\-]{3,12},([012]\d{1,2}|\d{1,2})(,[^]]+)?\]',
1709
				'before' => '<span style="text-shadow: $1 1px 1px 1px">',
1710
				'after' => '</span>',
1711
			),
1712
			// Legacy (alias of [color=green])
1713
			array(
1714
				'tag' => 'green',
1715
				'before' => '<span style="color: green;" class="bbc_color">',
1716
				'after' => '</span>',
1717
			),
1718
			array(
1719
				'tag' => 'html',
1720
				'type' => 'unparsed_content',
1721
				'content' => '<div>$1</div>',
1722
				'block_level' => true,
1723
				'disabled_content' => '$1',
1724
			),
1725
			array(
1726
				'tag' => 'hr',
1727
				'type' => 'closed',
1728
				'content' => '<hr>',
1729
				'block_level' => true,
1730
			),
1731
			array(
1732
				'tag' => 'i',
1733
				'before' => '<i>',
1734
				'after' => '</i>',
1735
			),
1736
			array(
1737
				'tag' => 'img',
1738
				'type' => 'unparsed_content',
1739
				'parameters' => array(
1740
					'alt' => array('optional' => true),
1741
					'title' => array('optional' => true),
1742
					'width' => array('optional' => true, 'value' => ' width="$1"', 'match' => '(\d+)'),
1743
					'height' => array('optional' => true, 'value' => ' height="$1"', 'match' => '(\d+)'),
1744
				),
1745
				'content' => '<img src="$1" alt="{alt}" title="{title}"{width}{height} class="bbc_img resized">',
1746
				'validate' => function(&$tag, &$data, $disabled)
1747
				{
1748
					$data = strtr($data, array('<br>' => ''));
1749
1750
					if (parse_url($data, PHP_URL_SCHEME) === null)
1751
						$data = '//' . ltrim($data, ':/');
1752
					else
1753
						$data = get_proxied_url($data);
1754
				},
1755
				'disabled_content' => '($1)',
1756
			),
1757
			array(
1758
				'tag' => 'img',
1759
				'type' => 'unparsed_content',
1760
				'content' => '<img src="$1" alt="" class="bbc_img">',
1761
				'validate' => function(&$tag, &$data, $disabled)
1762
				{
1763
					$data = strtr($data, array('<br>' => ''));
1764
1765
					if (parse_url($data, PHP_URL_SCHEME) === null)
1766
						$data = '//' . ltrim($data, ':/');
1767
					else
1768
						$data = get_proxied_url($data);
1769
				},
1770
				'disabled_content' => '($1)',
1771
			),
1772
			array(
1773
				'tag' => 'iurl',
1774
				'type' => 'unparsed_content',
1775
				'content' => '<a href="$1" class="bbc_link">$1</a>',
1776
				'validate' => function(&$tag, &$data, $disabled)
1777
				{
1778
					$data = strtr($data, array('<br>' => ''));
1779
					$scheme = parse_url($data, PHP_URL_SCHEME);
1780
					if (empty($scheme))
1781
						$data = '//' . ltrim($data, ':/');
1782
				},
1783
			),
1784
			array(
1785
				'tag' => 'iurl',
1786
				'type' => 'unparsed_equals',
1787
				'quoted' => 'optional',
1788
				'before' => '<a href="$1" class="bbc_link">',
1789
				'after' => '</a>',
1790
				'validate' => function(&$tag, &$data, $disabled)
1791
				{
1792
					if (substr($data, 0, 1) == '#')
1793
						$data = '#post_' . substr($data, 1);
1794
					else
1795
					{
1796
						$scheme = parse_url($data, PHP_URL_SCHEME);
1797
						if (empty($scheme))
1798
							$data = '//' . ltrim($data, ':/');
1799
					}
1800
				},
1801
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1802
				'disabled_after' => ' ($1)',
1803
			),
1804
			array(
1805
				'tag' => 'justify',
1806
				'before' => '<div class="justifytext">',
1807
				'after' => '</div>',
1808
				'block_level' => true,
1809
			),
1810
			array(
1811
				'tag' => 'left',
1812
				'before' => '<div class="lefttext">',
1813
				'after' => '</div>',
1814
				'block_level' => true,
1815
			),
1816
			array(
1817
				'tag' => 'li',
1818
				'before' => '<li>',
1819
				'after' => '</li>',
1820
				'trim' => 'outside',
1821
				'require_parents' => array('list'),
1822
				'block_level' => true,
1823
				'disabled_before' => '',
1824
				'disabled_after' => '<br>',
1825
			),
1826
			array(
1827
				'tag' => 'list',
1828
				'before' => '<ul class="bbc_list">',
1829
				'after' => '</ul>',
1830
				'trim' => 'inside',
1831
				'require_children' => array('li', 'list'),
1832
				'block_level' => true,
1833
			),
1834
			array(
1835
				'tag' => 'list',
1836
				'parameters' => array(
1837
					'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)'),
1838
				),
1839
				'before' => '<ul class="bbc_list" style="list-style-type: {type};">',
1840
				'after' => '</ul>',
1841
				'trim' => 'inside',
1842
				'require_children' => array('li'),
1843
				'block_level' => true,
1844
			),
1845
			array(
1846
				'tag' => 'ltr',
1847
				'before' => '<bdo dir="ltr">',
1848
				'after' => '</bdo>',
1849
				'block_level' => true,
1850
			),
1851
			array(
1852
				'tag' => 'me',
1853
				'type' => 'unparsed_equals',
1854
				'before' => '<div class="meaction">* $1 ',
1855
				'after' => '</div>',
1856
				'quoted' => 'optional',
1857
				'block_level' => true,
1858
				'disabled_before' => '/me ',
1859
				'disabled_after' => '<br>',
1860
			),
1861
			array(
1862
				'tag' => 'member',
1863
				'type' => 'unparsed_equals',
1864
				'before' => '<a href="' . $scripturl . '?action=profile;u=$1" class="mention" data-mention="$1">@',
1865
				'after' => '</a>',
1866
			),
1867
			// Legacy (horrible memories of the 1990s)
1868
			array(
1869
				'tag' => 'move',
1870
				'before' => '<marquee>',
1871
				'after' => '</marquee>',
1872
				'block_level' => true,
1873
				'disallow_children' => array('move'),
1874
			),
1875
			array(
1876
				'tag' => 'nobbc',
1877
				'type' => 'unparsed_content',
1878
				'content' => '$1',
1879
			),
1880
			array(
1881
				'tag' => 'php',
1882
				'type' => 'unparsed_content',
1883
				'content' => '<span class="phpcode">$1</span>',
1884
				'validate' => isset($disabled['php']) ? null : function(&$tag, &$data, $disabled)
1885
				{
1886
					if (!isset($disabled['php']))
1887
					{
1888
						$add_begin = substr(trim($data), 0, 5) != '&lt;?';
1889
						$data = highlight_php_code($add_begin ? '&lt;?php ' . $data . '?&gt;' : $data);
1890
						if ($add_begin)
1891
							$data = preg_replace(array('~^(.+?)&lt;\?.{0,40}?php(?:&nbsp;|\s)~', '~\?&gt;((?:</(font|span)>)*)$~'), '$1', $data, 2);
1892
					}
1893
				},
1894
				'block_level' => false,
1895
				'disabled_content' => '$1',
1896
			),
1897
			array(
1898
				'tag' => 'pre',
1899
				'before' => '<pre>',
1900
				'after' => '</pre>',
1901
			),
1902
			array(
1903
				'tag' => 'quote',
1904
				'before' => '<blockquote><cite>' . $txt['quote'] . '</cite>',
1905
				'after' => '</blockquote>',
1906
				'trim' => 'both',
1907
				'block_level' => true,
1908
			),
1909
			array(
1910
				'tag' => 'quote',
1911
				'parameters' => array(
1912
					'author' => array('match' => '(.{1,192}?)', 'quoted' => true),
1913
				),
1914
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1915
				'after' => '</blockquote>',
1916
				'trim' => 'both',
1917
				'block_level' => true,
1918
			),
1919
			array(
1920
				'tag' => 'quote',
1921
				'type' => 'parsed_equals',
1922
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': $1</cite>',
1923
				'after' => '</blockquote>',
1924
				'trim' => 'both',
1925
				'quoted' => 'optional',
1926
				// Don't allow everything to be embedded with the author name.
1927
				'parsed_tags_allowed' => array('url', 'iurl', 'ftp'),
1928
				'block_level' => true,
1929
			),
1930
			array(
1931
				'tag' => 'quote',
1932
				'parameters' => array(
1933
					'author' => array('match' => '([^<>]{1,192}?)'),
1934
					'link' => array('match' => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|msg=\d+?|action=profile;u=\d+)'),
1935
					'date' => array('match' => '(\d+)', 'validate' => 'timeformat'),
1936
				),
1937
				'before' => '<blockquote><cite><a href="' . $scripturl . '?{link}">' . $txt['quote_from'] . ': {author} ' . $txt['search_on'] . ' {date}</a></cite>',
1938
				'after' => '</blockquote>',
1939
				'trim' => 'both',
1940
				'block_level' => true,
1941
			),
1942
			array(
1943
				'tag' => 'quote',
1944
				'parameters' => array(
1945
					'author' => array('match' => '(.{1,192}?)'),
1946
				),
1947
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1948
				'after' => '</blockquote>',
1949
				'trim' => 'both',
1950
				'block_level' => true,
1951
			),
1952
			// Legacy (alias of [color=red])
1953
			array(
1954
				'tag' => 'red',
1955
				'before' => '<span style="color: red;" class="bbc_color">',
1956
				'after' => '</span>',
1957
			),
1958
			array(
1959
				'tag' => 'right',
1960
				'before' => '<div class="righttext">',
1961
				'after' => '</div>',
1962
				'block_level' => true,
1963
			),
1964
			array(
1965
				'tag' => 'rtl',
1966
				'before' => '<bdo dir="rtl">',
1967
				'after' => '</bdo>',
1968
				'block_level' => true,
1969
			),
1970
			array(
1971
				'tag' => 's',
1972
				'before' => '<s>',
1973
				'after' => '</s>',
1974
			),
1975
			// Legacy (never a good idea)
1976
			array(
1977
				'tag' => 'shadow',
1978
				'type' => 'unparsed_commas',
1979
				'test' => '[#0-9a-zA-Z\-]{3,12},(left|right|top|bottom|[0123]\d{0,2})\]',
1980
				'before' => '<span style="text-shadow: $1 $2">',
1981
				'after' => '</span>',
1982
				'validate' => function(&$tag, &$data, $disabled)
1983
				{
1984
1985
					if ($data[1] == 'top' || (is_numeric($data[1]) && $data[1] < 50))
1986
						$data[1] = '0 -2px 1px';
1987
1988
					elseif ($data[1] == 'right' || (is_numeric($data[1]) && $data[1] < 100))
1989
						$data[1] = '2px 0 1px';
1990
1991
					elseif ($data[1] == 'bottom' || (is_numeric($data[1]) && $data[1] < 190))
1992
						$data[1] = '0 2px 1px';
1993
1994
					elseif ($data[1] == 'left' || (is_numeric($data[1]) && $data[1] < 280))
1995
						$data[1] = '-2px 0 1px';
1996
1997
					else
1998
						$data[1] = '1px 1px 1px';
1999
				},
2000
			),
2001
			array(
2002
				'tag' => 'size',
2003
				'type' => 'unparsed_equals',
2004
				'test' => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)\]',
2005
				'before' => '<span style="font-size: $1;" class="bbc_size">',
2006
				'after' => '</span>',
2007
			),
2008
			array(
2009
				'tag' => 'size',
2010
				'type' => 'unparsed_equals',
2011
				'test' => '[1-7]\]',
2012
				'before' => '<span style="font-size: $1;" class="bbc_size">',
2013
				'after' => '</span>',
2014
				'validate' => function(&$tag, &$data, $disabled)
2015
				{
2016
					$sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95);
2017
					$data = $sizes[$data] . 'em';
2018
				},
2019
			),
2020
			array(
2021
				'tag' => 'sub',
2022
				'before' => '<sub>',
2023
				'after' => '</sub>',
2024
			),
2025
			array(
2026
				'tag' => 'sup',
2027
				'before' => '<sup>',
2028
				'after' => '</sup>',
2029
			),
2030
			array(
2031
				'tag' => 'table',
2032
				'before' => '<table class="bbc_table">',
2033
				'after' => '</table>',
2034
				'trim' => 'inside',
2035
				'require_children' => array('tr'),
2036
				'block_level' => true,
2037
			),
2038
			array(
2039
				'tag' => 'td',
2040
				'before' => '<td>',
2041
				'after' => '</td>',
2042
				'require_parents' => array('tr'),
2043
				'trim' => 'outside',
2044
				'block_level' => true,
2045
				'disabled_before' => '',
2046
				'disabled_after' => '',
2047
			),
2048
			array(
2049
				'tag' => 'time',
2050
				'type' => 'unparsed_content',
2051
				'content' => '$1',
2052
				'validate' => function(&$tag, &$data, $disabled)
2053
				{
2054
					if (is_numeric($data))
2055
						$data = timeformat($data);
2056
					else
2057
						$tag['content'] = '[time]$1[/time]';
2058
				},
2059
			),
2060
			array(
2061
				'tag' => 'tr',
2062
				'before' => '<tr>',
2063
				'after' => '</tr>',
2064
				'require_parents' => array('table'),
2065
				'require_children' => array('td'),
2066
				'trim' => 'both',
2067
				'block_level' => true,
2068
				'disabled_before' => '',
2069
				'disabled_after' => '',
2070
			),
2071
			// Legacy (the <tt> element is dead)
2072
			array(
2073
				'tag' => 'tt',
2074
				'before' => '<span class="monospace">',
2075
				'after' => '</span>',
2076
			),
2077
			array(
2078
				'tag' => 'u',
2079
				'before' => '<u>',
2080
				'after' => '</u>',
2081
			),
2082
			array(
2083
				'tag' => 'url',
2084
				'type' => 'unparsed_content',
2085
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
2086
				'validate' => function(&$tag, &$data, $disabled)
2087
				{
2088
					$data = strtr($data, array('<br>' => ''));
2089
					$scheme = parse_url($data, PHP_URL_SCHEME);
2090
					if (empty($scheme))
2091
						$data = '//' . ltrim($data, ':/');
2092
				},
2093
			),
2094
			array(
2095
				'tag' => 'url',
2096
				'type' => 'unparsed_equals',
2097
				'quoted' => 'optional',
2098
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
2099
				'after' => '</a>',
2100
				'validate' => function(&$tag, &$data, $disabled)
2101
				{
2102
					$scheme = parse_url($data, PHP_URL_SCHEME);
2103
					if (empty($scheme))
2104
						$data = '//' . ltrim($data, ':/');
2105
				},
2106
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
2107
				'disabled_after' => ' ($1)',
2108
			),
2109
			// Legacy (alias of [color=white])
2110
			array(
2111
				'tag' => 'white',
2112
				'before' => '<span style="color: white;" class="bbc_color">',
2113
				'after' => '</span>',
2114
			),
2115
			array(
2116
				'tag' => 'youtube',
2117
				'type' => 'unparsed_content',
2118
				'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>',
2119
				'disabled_content' => '<a href="https://www.youtube.com/watch?v=$1" target="_blank" rel="noopener">https://www.youtube.com/watch?v=$1</a>',
2120
				'block_level' => true,
2121
			),
2122
		);
2123
2124
		// Inside these tags autolink is not recommendable.
2125
		$no_autolink_tags = array(
2126
			'url',
2127
			'iurl',
2128
			'email',
2129
			'img',
2130
			'html',
2131
		);
2132
2133
		// Let mods add new BBC without hassle.
2134
		call_integration_hook('integrate_bbc_codes', array(&$codes, &$no_autolink_tags));
2135
2136
		// This is mainly for the bbc manager, so it's easy to add tags above.  Custom BBC should be added above this line.
2137
		if ($message === false)
0 ignored issues
show
introduced by
The condition $message === false is always false.
Loading history...
2138
		{
2139
			usort($codes, function($a, $b)
2140
			{
2141
				return strcmp($a['tag'], $b['tag']);
2142
			});
2143
			return $codes;
2144
		}
2145
2146
		// So the parser won't skip them.
2147
		$itemcodes = array(
2148
			'*' => 'disc',
2149
			'@' => 'disc',
2150
			'+' => 'square',
2151
			'x' => 'square',
2152
			'#' => 'square',
2153
			'o' => 'circle',
2154
			'O' => 'circle',
2155
			'0' => 'circle',
2156
		);
2157
		if (!isset($disabled['li']) && !isset($disabled['list']))
2158
		{
2159
			foreach ($itemcodes as $c => $dummy)
2160
				$bbc_codes[$c] = array();
2161
		}
2162
2163
		// Shhhh!
2164
		if (!isset($disabled['color']))
2165
		{
2166
			$codes[] = array(
2167
				'tag' => 'chrissy',
2168
				'before' => '<span style="color: #cc0099;">',
2169
				'after' => ' :-*</span>',
2170
			);
2171
			$codes[] = array(
2172
				'tag' => 'kissy',
2173
				'before' => '<span style="color: #cc0099;">',
2174
				'after' => ' :-*</span>',
2175
			);
2176
		}
2177
		$codes[] = array(
2178
			'tag' => 'cowsay',
2179
			'parameters' => array(
2180
				'e' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => 'oo', 'validate' => function ($eyes) use ($smcFunc)
2181
					{
2182
						return $smcFunc['substr']($eyes . 'oo', 0, 2);
2183
					},
2184
				),
2185
				't' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => '  ', 'validate' => function ($tongue) use ($smcFunc)
2186
					{
2187
						return $smcFunc['substr']($tongue . '  ', 0, 2);
2188
					},
2189
				),
2190
			),
2191
			'before' => '<pre data-e="{e}" data-t="{t}"><div>',
2192
			'after' => '</div><script>' . '$("head").append("<style>" + ' . JavaScriptEscape(base64_decode('cHJlW2RhdGEtZV1bZGF0YS10XXt3aGl0ZS1zcGFjZTpwcmUtd3JhcDtsaW5lLWhlaWdodDppbml0aWFsO31wcmVbZGF0YS1lXVtkYXRhLXRdID4gZGl2e2Rpc3BsYXk6dGFibGU7Ym9yZGVyOjFweCBzb2xpZDtib3JkZXItcmFkaXVzOjAuNWVtO3BhZGRpbmc6MWNoO21heC13aWR0aDo4MGNoO21pbi13aWR0aDoxMmNoO31wcmVbZGF0YS1lXVtkYXRhLXRdOjphZnRlcntkaXNwbGF5OmlubGluZS1ibG9jazttYXJnaW4tbGVmdDo4Y2g7bWluLXdpZHRoOjIwY2g7ZGlyZWN0aW9uOmx0cjtjb250ZW50OidcNUMgJycgJycgXl9fXlxBICcnIFw1QyAnJyAoJyBhdHRyKGRhdGEtZSkgJylcNUNfX19fX19fXEEgJycgJycgJycgKF9fKVw1QyAnJyAnJyAnJyAnJyAnJyAnJyAnJyApXDVDL1w1Q1xBICcnICcnICcnICcnICcgYXR0cihkYXRhLXQpICcgfHwtLS0tdyB8XEEgJycgJycgJycgJycgJycgJycgJycgfHwgJycgJycgJycgJycgfHwnO30=')) . ' + "</style>");' . '</script></pre>',
2193
			'block_level' => true,
2194
		);
2195
2196
		foreach ($codes as $code)
2197
		{
2198
			// Make it easier to process parameters later
2199
			if (!empty($code['parameters']))
2200
				ksort($code['parameters'], SORT_STRING);
2201
2202
			// If we are not doing every tag only do ones we are interested in.
2203
			if (empty($parse_tags) || in_array($code['tag'], $parse_tags))
2204
				$bbc_codes[substr($code['tag'], 0, 1)][] = $code;
2205
		}
2206
		$codes = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $codes is dead and can be removed.
Loading history...
2207
	}
2208
2209
	// Shall we take the time to cache this?
2210
	if ($cache_id != '' && !empty($cache_enable) && (($cache_enable >= 2 && isset($message[1000])) || isset($message[2400])) && empty($parse_tags))
2211
	{
2212
		// It's likely this will change if the message is modified.
2213
		$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']);
2214
2215
		if (($temp = cache_get_data($cache_key, 240)) != null)
2216
			return $temp;
2217
2218
		$cache_t = microtime(true);
2219
	}
2220
2221
	if ($smileys === 'print')
0 ignored issues
show
introduced by
The condition $smileys === 'print' is always false.
Loading history...
2222
	{
2223
		// [glow], [shadow], and [move] can't really be printed.
2224
		$disabled['glow'] = true;
2225
		$disabled['shadow'] = true;
2226
		$disabled['move'] = true;
2227
2228
		// Colors can't well be displayed... supposed to be black and white.
2229
		$disabled['color'] = true;
2230
		$disabled['black'] = true;
2231
		$disabled['blue'] = true;
2232
		$disabled['white'] = true;
2233
		$disabled['red'] = true;
2234
		$disabled['green'] = true;
2235
		$disabled['me'] = true;
2236
2237
		// Color coding doesn't make sense.
2238
		$disabled['php'] = true;
2239
2240
		// Links are useless on paper... just show the link.
2241
		$disabled['ftp'] = true;
2242
		$disabled['url'] = true;
2243
		$disabled['iurl'] = true;
2244
		$disabled['email'] = true;
2245
		$disabled['flash'] = true;
2246
2247
		// @todo Change maybe?
2248
		if (!isset($_GET['images']))
2249
			$disabled['img'] = true;
2250
2251
		// Maybe some custom BBC need to be disabled for printing.
2252
		call_integration_hook('integrate_bbc_print', array(&$disabled));
2253
	}
2254
2255
	$open_tags = array();
2256
	$message = strtr($message, array("\n" => '<br>'));
2257
2258
	if (!empty($parse_tags))
2259
	{
2260
		$real_alltags_regex = $alltags_regex;
2261
		$alltags_regex = '';
2262
	}
2263
	if (empty($alltags_regex))
2264
	{
2265
		$alltags = array();
2266
		foreach ($bbc_codes as $section)
2267
		{
2268
			foreach ($section as $code)
2269
				$alltags[] = $code['tag'];
2270
		}
2271
		$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

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

2271
		$alltags_regex = '(?' . '>\b' . /** @scrutinizer ignore-type */ build_regex(array_unique($alltags)) . '\b|' . build_regex(array_keys($itemcodes)) . ')';
Loading history...
2272
	}
2273
2274
	$pos = -1;
2275
	while ($pos !== false)
2276
	{
2277
		$last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos;
2278
		preg_match('~\[/?(?=' . $alltags_regex . ')~i', $message, $matches, PREG_OFFSET_CAPTURE, $pos + 1);
2279
		$pos = isset($matches[0][1]) ? $matches[0][1] : false;
2280
2281
		// Failsafe.
2282
		if ($pos === false || $last_pos > $pos)
2283
			$pos = strlen($message) + 1;
2284
2285
		// Can't have a one letter smiley, URL, or email! (Sorry.)
2286
		if ($last_pos < $pos - 1)
2287
		{
2288
			// Make sure the $last_pos is not negative.
2289
			$last_pos = max($last_pos, 0);
2290
2291
			// Pick a block of data to do some raw fixing on.
2292
			$data = substr($message, $last_pos, $pos - $last_pos);
2293
2294
			$placeholders = array();
2295
			$placeholders_counter = 0;
2296
2297
			// Take care of some HTML!
2298
			if (!empty($modSettings['enablePostHTML']) && strpos($data, '&lt;') !== false)
2299
			{
2300
				$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);
2301
2302
				// <br> should be empty.
2303
				$empty_tags = array('br', 'hr');
2304
				foreach ($empty_tags as $tag)
2305
					$data = str_replace(array('&lt;' . $tag . '&gt;', '&lt;' . $tag . '/&gt;', '&lt;' . $tag . ' /&gt;'), '<' . $tag . '>', $data);
2306
2307
				// b, u, i, s, pre... basic tags.
2308
				$closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote', 'strong');
2309
				foreach ($closable_tags as $tag)
2310
				{
2311
					$diff = substr_count($data, '&lt;' . $tag . '&gt;') - substr_count($data, '&lt;/' . $tag . '&gt;');
2312
					$data = strtr($data, array('&lt;' . $tag . '&gt;' => '<' . $tag . '>', '&lt;/' . $tag . '&gt;' => '</' . $tag . '>'));
2313
2314
					if ($diff > 0)
2315
						$data = substr($data, 0, -1) . str_repeat('</' . $tag . '>', $diff) . substr($data, -1);
2316
				}
2317
2318
				// Do <img ...> - with security... action= -> action-.
2319
				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);
2320
				if (!empty($matches[0]))
2321
				{
2322
					$replaces = array();
2323
					foreach ($matches[2] as $match => $imgtag)
2324
					{
2325
						$alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^&quot;|&quot;$~', '', $matches[3][$match]);
2326
2327
						// Remove action= from the URL - no funny business, now.
2328
						// @todo Testing this preg_match seems pointless
2329
						if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) != 0)
2330
							$imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag);
2331
2332
						$placeholder = '<placeholder ' . ++$placeholders_counter . '>';
2333
						$placeholders[$placeholder] = '[img' . $alt . ']' . $imgtag . '[/img]';
2334
2335
						$replaces[$matches[0][$match]] = $placeholder;
2336
					}
2337
2338
					$data = strtr($data, $replaces);
2339
				}
2340
			}
2341
2342
			if (!empty($modSettings['autoLinkUrls']))
2343
			{
2344
				// Are we inside tags that should be auto linked?
2345
				$no_autolink_area = false;
2346
				if (!empty($open_tags))
2347
				{
2348
					foreach ($open_tags as $open_tag)
2349
						if (in_array($open_tag['tag'], $no_autolink_tags))
2350
							$no_autolink_area = true;
2351
				}
2352
2353
				// Don't go backwards.
2354
				// @todo Don't think is the real solution....
2355
				$lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0;
2356
				if ($pos < $lastAutoPos)
2357
					$no_autolink_area = true;
2358
				$lastAutoPos = $pos;
2359
2360
				if (!$no_autolink_area)
2361
				{
2362
					// An &nbsp; right after a URL can break the autolinker
2363
					if (strpos($data, '&nbsp;') !== false)
2364
					{
2365
						$placeholders['<placeholder non-breaking-space>'] = '&nbsp;';
2366
						$data = strtr($data, array('&nbsp;' => '<placeholder non-breaking-space>'));
2367
					}
2368
2369
					// Parse any URLs
2370
					if (!isset($disabled['url']) && strpos($data, '[url') === false)
2371
					{
2372
						// For efficiency, first define the TLD regex in a PCRE subroutine
2373
						$url_regex = '(?(DEFINE)(?<tlds>' . $modSettings['tld_regex'] . '))';
2374
2375
						// Now build the rest of the regex
2376
						$url_regex .=
2377
						// 1. IRI scheme and domain components
2378
						'(?:' .
2379
							// 1a. IRIs with a scheme, or at least an opening "//"
2380
							'(?:' .
2381
2382
								// URI scheme (or lack thereof for schemeless URLs)
2383
								'(?:' .
2384
									// URL scheme and colon
2385
									'\b[a-z][\w\-]+:' .
2386
									// or
2387
									'|' .
2388
									// A boundary followed by two slashes for schemeless URLs
2389
									'(?<=^|\W)(?=//)' .
2390
								')' .
2391
2392
								// IRI "authority" chunk
2393
								'(?:' .
2394
									// 2 slashes for IRIs with an "authority"
2395
									'//' .
2396
									// then a domain name
2397
									'(?:' .
2398
										// Either the reserved "localhost" domain name
2399
										'localhost' .
2400
										// or
2401
										'|' .
2402
										// a run of IRI characters, a dot, and a TLD
2403
										'[\p{L}\p{M}\p{N}\-.:@]+\.(?P>tlds)' .
2404
									')' .
2405
									// followed by a non-domain character or end of line
2406
									'(?=[^\p{L}\p{N}\-.]|$)' .
2407
2408
									// or, if no "authority" per se (e.g. "mailto:" URLs)...
2409
									'|' .
2410
2411
									// a run of IRI characters
2412
									'[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.:@]+[\p{L}\p{M}\p{N}]' .
2413
									// and then a dot and a closing IRI label
2414
									'\.[\p{L}\p{M}\p{N}\-]+' .
2415
								')' .
2416
							')' .
2417
2418
							// Or
2419
							'|' .
2420
2421
							// 1b. Naked domains (e.g. "example.com" in "Go to example.com for an example.")
2422
							'(?:' .
2423
								// Preceded by start of line or a non-domain character
2424
								'(?<=^|[^\p{L}\p{M}\p{N}\-:@])' .
2425
								// A run of Unicode domain name characters (excluding [:@])
2426
								'[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.]+[\p{L}\p{M}\p{N}]' .
2427
								// and then a dot and a valid TLD
2428
								'\.(?P>tlds)' .
2429
								// Followed by either:
2430
								'(?=' .
2431
									// end of line or a non-domain character (excluding [.:@])
2432
									'$|[^\p{L}\p{N}\-]' .
2433
									// or
2434
									'|' .
2435
									// a dot followed by end of line or a non-domain character (excluding [.:@])
2436
									'\.(?=$|[^\p{L}\p{N}\-])' .
2437
								')' .
2438
							')' .
2439
						')' .
2440
2441
						// 2. IRI path, query, and fragment components (if present)
2442
						'(?:' .
2443
2444
							// If any of these parts exist, must start with a single "/"
2445
							'/' .
2446
2447
							// And then optionally:
2448
							'(?:' .
2449
								// One or more of:
2450
								'(?:' .
2451
									// a run of non-space, non-()<>
2452
									'[^\s()<>]+' .
2453
									// or
2454
									'|' .
2455
									// balanced parentheses, up to 2 levels
2456
									'\(([^\s()<>]+|(\([^\s()<>]+\)))*\)' .
2457
								')+' .
2458
								// Ending with:
2459
								'(?:' .
2460
									// balanced parentheses, up to 2 levels
2461
									'\(([^\s()<>]+|(\([^\s()<>]+\)))*\)' .
2462
									// or
2463
									'|' .
2464
									// not a space or one of these punctuation characters
2465
									'[^\s`!()\[\]{};:\'".,<>?«»“”‘’/]' .
2466
									// or
2467
									'|' .
2468
									// a trailing slash (but not two in a row)
2469
									'(?<!/)/' .
2470
								')' .
2471
							')?' .
2472
						')?';
2473
2474
						$data = preg_replace_callback('~' . $url_regex . '~i' . ($context['utf8'] ? 'u' : ''), function($matches)
2475
						{
2476
							$url = array_shift($matches);
2477
2478
							// If this isn't a clean URL, bail out
2479
							if ($url != sanitize_iri($url))
2480
								return $url;
2481
2482
							$scheme = parse_url($url, PHP_URL_SCHEME);
2483
2484
							if ($scheme == 'mailto')
2485
							{
2486
								$email_address = str_replace('mailto:', '', $url);
2487
								if (!isset($disabled['email']) && filter_var($email_address, FILTER_VALIDATE_EMAIL) !== false)
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...
2488
									return '[email=' . $email_address . ']' . $url . '[/email]';
2489
								else
2490
									return $url;
2491
							}
2492
2493
							// Are we linking a schemeless URL or naked domain name (e.g. "example.com")?
2494
							if (empty($scheme))
2495
								$fullUrl = '//' . ltrim($url, ':/');
2496
							else
2497
								$fullUrl = $url;
2498
2499
							// Make sure that $fullUrl really is valid
2500
							if (validate_iri((strpos($fullUrl, '//') === 0 ? 'http:' : '') . $fullUrl) === false)
2501
								return $url;
2502
2503
							return '[url=&quot;' . str_replace(array('[', ']'), array('&#91;', '&#93;'), $fullUrl) . '&quot;]' . $url . '[/url]';
2504
						}, $data);
2505
					}
2506
2507
					// Next, emails...  Must be careful not to step on enablePostHTML logic above...
2508
					if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false && stripos($data, 'mailto:') === false)
2509
					{
2510
						$email_regex = '
2511
						# Preceded by a non-domain character or start of line
2512
						(?<=^|[^\p{L}\p{M}\p{N}\-\.])
2513
2514
						# An email address
2515
						[\p{L}\p{M}\p{N}_\-.]{1,80}
2516
						@
2517
						[\p{L}\p{M}\p{N}\-.]+
2518
						\.
2519
						' . $modSettings['tld_regex'] . '
2520
2521
						# Followed by either:
2522
						(?=
2523
							# end of line or a non-domain character (excluding the dot)
2524
							$|[^\p{L}\p{M}\p{N}\-]
2525
							| # or
2526
							# a dot followed by end of line or a non-domain character
2527
							\.(?=$|[^\p{L}\p{M}\p{N}\-])
2528
						)';
2529
2530
						$data = preg_replace('~' . $email_regex . '~xi' . ($context['utf8'] ? 'u' : ''), '[email]$0[/email]', $data);
2531
					}
2532
				}
2533
			}
2534
2535
			// Restore any placeholders
2536
			$data = strtr($data, $placeholders);
2537
2538
			$data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));
2539
2540
			// If it wasn't changed, no copying or other boring stuff has to happen!
2541
			if ($data != substr($message, $last_pos, $pos - $last_pos))
2542
			{
2543
				$message = substr($message, 0, $last_pos) . $data . substr($message, $pos);
2544
2545
				// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
2546
				$old_pos = strlen($data) + $last_pos;
2547
				$pos = strpos($message, '[', $last_pos);
2548
				$pos = $pos === false ? $old_pos : min($pos, $old_pos);
2549
			}
2550
		}
2551
2552
		// Are we there yet?  Are we there yet?
2553
		if ($pos >= strlen($message) - 1)
2554
			break;
2555
2556
		$tag_character = strtolower($message[$pos + 1]);
2557
2558
		if ($tag_character == '/' && !empty($open_tags))
2559
		{
2560
			$pos2 = strpos($message, ']', $pos + 1);
2561
			if ($pos2 == $pos + 2)
2562
				continue;
2563
2564
			$look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));
2565
2566
			// A closing tag that doesn't match any open tags? Skip it.
2567
			if (!in_array($look_for, array_map(function($code)
2568
			{
2569
				return $code['tag'];
2570
			}, $open_tags)))
2571
				continue;
2572
2573
			$to_close = array();
2574
			$block_level = null;
2575
2576
			do
2577
			{
2578
				$tag = array_pop($open_tags);
2579
				if (!$tag)
2580
					break;
2581
2582
				if (!empty($tag['block_level']))
2583
				{
2584
					// Only find out if we need to.
2585
					if ($block_level === false)
2586
					{
2587
						array_push($open_tags, $tag);
2588
						break;
2589
					}
2590
2591
					// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
2592
					if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]]))
2593
					{
2594
						foreach ($bbc_codes[$look_for[0]] as $temp)
2595
							if ($temp['tag'] == $look_for)
2596
							{
2597
								$block_level = !empty($temp['block_level']);
2598
								break;
2599
							}
2600
					}
2601
2602
					if ($block_level !== true)
2603
					{
2604
						$block_level = false;
2605
						array_push($open_tags, $tag);
2606
						break;
2607
					}
2608
				}
2609
2610
				$to_close[] = $tag;
2611
			}
2612
			while ($tag['tag'] != $look_for);
2613
2614
			// Did we just eat through everything and not find it?
2615
			if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
2616
			{
2617
				$open_tags = $to_close;
2618
				continue;
2619
			}
2620
			elseif (!empty($to_close) && $tag['tag'] != $look_for)
2621
			{
2622
				if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]]))
2623
				{
2624
					foreach ($bbc_codes[$look_for[0]] as $temp)
2625
						if ($temp['tag'] == $look_for)
2626
						{
2627
							$block_level = !empty($temp['block_level']);
2628
							break;
2629
						}
2630
				}
2631
2632
				// We're not looking for a block level tag (or maybe even a tag that exists...)
2633
				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...
2634
				{
2635
					foreach ($to_close as $tag)
2636
						array_push($open_tags, $tag);
2637
					continue;
2638
				}
2639
			}
2640
2641
			foreach ($to_close as $tag)
2642
			{
2643
				$message = substr($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1);
2644
				$pos += strlen($tag['after']) + 2;
2645
				$pos2 = $pos - 1;
2646
2647
				// See the comment at the end of the big loop - just eating whitespace ;).
2648
				$whitespace_regex = '';
2649
				if (!empty($tag['block_level']))
2650
					$whitespace_regex .= '(&nbsp;|\s)*(<br\s*/?' . '>)?';
2651
				// Trim one line of whitespace after unnested tags, but all of it after nested ones
2652
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
2653
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2654
2655
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
2656
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2657
			}
2658
2659
			if (!empty($to_close))
2660
			{
2661
				$to_close = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $to_close is dead and can be removed.
Loading history...
2662
				$pos--;
2663
			}
2664
2665
			continue;
2666
		}
2667
2668
		// No tags for this character, so just keep going (fastest possible course.)
2669
		if (!isset($bbc_codes[$tag_character]))
2670
			continue;
2671
2672
		$inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
2673
		$tag = null;
2674
		foreach ($bbc_codes[$tag_character] as $possible)
2675
		{
2676
			$pt_strlen = strlen($possible['tag']);
2677
2678
			// Not a match?
2679
			if (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag'])
2680
				continue;
2681
2682
			$next_c = isset($message[$pos + 1 + $pt_strlen]) ? $message[$pos + 1 + $pt_strlen] : '';
2683
2684
			// A tag is the last char maybe
2685
			if ($next_c == '')
2686
				break;
2687
2688
			// A test validation?
2689
			if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0)
2690
				continue;
2691
			// Do we want parameters?
2692
			elseif (!empty($possible['parameters']))
2693
			{
2694
				// Are all the parameters optional?
2695
				$param_required = false;
2696
				foreach ($possible['parameters'] as $param)
2697
				{
2698
					if (empty($param['optional']))
2699
					{
2700
						$param_required = true;
2701
						break;
2702
					}
2703
				}
2704
2705
				if ($param_required && $next_c != ' ')
2706
					continue;
2707
			}
2708
			elseif (isset($possible['type']))
2709
			{
2710
				// Do we need an equal sign?
2711
				if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=')
2712
					continue;
2713
				// Maybe we just want a /...
2714
				if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + $pt_strlen, 2) != '/]' && substr($message, $pos + 1 + $pt_strlen, 3) != ' /]')
2715
					continue;
2716
				// An immediate ]?
2717
				if ($possible['type'] == 'unparsed_content' && $next_c != ']')
2718
					continue;
2719
			}
2720
			// No type means 'parsed_content', which demands an immediate ] without parameters!
2721
			elseif ($next_c != ']')
2722
				continue;
2723
2724
			// Check allowed tree?
2725
			if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
2726
				continue;
2727
			elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
2728
				continue;
2729
			// If this is in the list of disallowed child tags, don't parse it.
2730
			elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
2731
				continue;
2732
2733
			$pos1 = $pos + 1 + $pt_strlen + 1;
2734
2735
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
2736
			if ($possible['tag'] == 'quote')
2737
			{
2738
				// Start with standard
2739
				$quote_alt = false;
2740
				foreach ($open_tags as $open_quote)
2741
				{
2742
					// Every parent quote this quote has flips the styling
2743
					if ($open_quote['tag'] == 'quote')
2744
						$quote_alt = !$quote_alt;
0 ignored issues
show
introduced by
The condition $quote_alt is always false.
Loading history...
2745
				}
2746
				// Add a class to the quote to style alternating blockquotes
2747
				$possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">'));
2748
			}
2749
2750
			// This is long, but it makes things much easier and cleaner.
2751
			if (!empty($possible['parameters']))
2752
			{
2753
				// Build a regular expression for each parameter for the current tag.
2754
				$regex_key = $smcFunc['json_encode']($possible['parameters']);
2755
				if (!isset($params_regexes[$regex_key]))
2756
				{
2757
					$params_regexes[$regex_key] = '';
2758
2759
					foreach ($possible['parameters'] as $p => $info)
2760
						$params_regexes[$regex_key] .= '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . '\s*)' . (empty($info['optional']) ? '' : '?');
2761
				}
2762
2763
				// Extract the string that potentially holds our parameters.
2764
				$blob = preg_split('~\[/?(?:' . $alltags_regex . ')~i', substr($message, $pos));
2765
				$blobs = preg_split('~\]~i', $blob[1]);
2766
2767
				$splitters = implode('=|', array_keys($possible['parameters'])) . '=';
2768
2769
				// Progressively append more blobs until we find our parameters or run out of blobs
2770
				$blob_counter = 1;
2771
				while ($blob_counter <= count($blobs))
0 ignored issues
show
Bug introduced by
It seems like $blobs can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, 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
				while ($blob_counter <= count(/** @scrutinizer ignore-type */ $blobs))
Loading history...
2772
				{
2773
					$given_param_string = implode(']', array_slice($blobs, 0, $blob_counter++));
0 ignored issues
show
Bug introduced by
It seems like $blobs can also be of type false; however, parameter $array of array_slice() does only seem to accept array, 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

2773
					$given_param_string = implode(']', array_slice(/** @scrutinizer ignore-type */ $blobs, 0, $blob_counter++));
Loading history...
2774
2775
					$given_params = preg_split('~\s(?=(' . $splitters . '))~i', $given_param_string);
2776
					sort($given_params, SORT_STRING);
0 ignored issues
show
Bug introduced by
It seems like $given_params can also be of type false; however, parameter $array of sort() does only seem to accept array, 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

2776
					sort(/** @scrutinizer ignore-type */ $given_params, SORT_STRING);
Loading history...
2777
2778
					$match = preg_match('~^' . $params_regexes[$regex_key] . '$~i', implode(' ', $given_params), $matches) !== 0;
2779
2780
					if ($match)
2781
						break;
2782
				}
2783
2784
				// Didn't match our parameter list, try the next possible.
2785
				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...
2786
					continue;
2787
2788
				$params = array();
2789
				for ($i = 1, $n = count($matches); $i < $n; $i += 2)
2790
				{
2791
					$key = strtok(ltrim($matches[$i]), '=');
2792
					if ($key === false)
2793
						continue;
2794
					elseif (isset($possible['parameters'][$key]['value']))
2795
						$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
2796
					elseif (isset($possible['parameters'][$key]['validate']))
2797
						$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
2798
					else
2799
						$params['{' . $key . '}'] = $matches[$i + 1];
2800
2801
					// Just to make sure: replace any $ or { so they can't interpolate wrongly.
2802
					$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
2803
				}
2804
2805
				foreach ($possible['parameters'] as $p => $info)
2806
				{
2807
					if (!isset($params['{' . $p . '}']))
2808
					{
2809
						if (!isset($info['default']))
2810
							$params['{' . $p . '}'] = '';
2811
						elseif (isset($possible['parameters'][$p]['value']))
2812
							$params['{' . $p . '}'] = strtr($possible['parameters'][$p]['value'], array('$1' => $info['default']));
2813
						elseif (isset($possible['parameters'][$p]['validate']))
2814
							$params['{' . $p . '}'] = $possible['parameters'][$p]['validate']($info['default']);
2815
						else
2816
							$params['{' . $p . '}'] = $info['default'];
2817
					}
2818
				}
2819
2820
				$tag = $possible;
2821
2822
				// Put the parameters into the string.
2823
				if (isset($tag['before']))
2824
					$tag['before'] = strtr($tag['before'], $params);
2825
				if (isset($tag['after']))
2826
					$tag['after'] = strtr($tag['after'], $params);
2827
				if (isset($tag['content']))
2828
					$tag['content'] = strtr($tag['content'], $params);
2829
2830
				$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...
2831
			}
2832
			else
2833
			{
2834
				$tag = $possible;
2835
				$params = array();
2836
			}
2837
			break;
2838
		}
2839
2840
		// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
2841
		if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
2842
		{
2843
			if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
2844
				continue;
2845
2846
			$tag = $itemcodes[$message[$pos + 1]];
2847
2848
			// First let's set up the tree: it needs to be in a list, or after an li.
2849
			if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
2850
			{
2851
				$open_tags[] = array(
2852
					'tag' => 'list',
2853
					'after' => '</ul>',
2854
					'block_level' => true,
2855
					'require_children' => array('li'),
2856
					'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2857
				);
2858
				$code = '<ul class="bbc_list">';
2859
			}
2860
			// We're in a list item already: another itemcode?  Close it first.
2861
			elseif ($inside['tag'] == 'li')
2862
			{
2863
				array_pop($open_tags);
2864
				$code = '</li>';
2865
			}
2866
			else
2867
				$code = '';
2868
2869
			// Now we open a new tag.
2870
			$open_tags[] = array(
2871
				'tag' => 'li',
2872
				'after' => '</li>',
2873
				'trim' => 'outside',
2874
				'block_level' => true,
2875
				'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2876
			);
2877
2878
			// First, open the tag...
2879
			$code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
2880
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
2881
			$pos += strlen($code) - 1 + 2;
2882
2883
			// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
2884
			$pos2 = strpos($message, '<br>', $pos);
2885
			$pos3 = strpos($message, '[/', $pos);
2886
			if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
2887
			{
2888
				preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 4), $matches);
2889
				$message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
2890
2891
				$open_tags[count($open_tags) - 2]['after'] = '</ul>';
2892
			}
2893
			// Tell the [list] that it needs to close specially.
2894
			else
2895
			{
2896
				// Move the li over, because we're not sure what we'll hit.
2897
				$open_tags[count($open_tags) - 1]['after'] = '';
2898
				$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
2899
			}
2900
2901
			continue;
2902
		}
2903
2904
		// Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
2905
		if ($tag === null && $inside !== null && !empty($inside['require_children']))
2906
		{
2907
			array_pop($open_tags);
2908
2909
			$message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
2910
			$pos += strlen($inside['after']) - 1 + 2;
2911
		}
2912
2913
		// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
2914
		if ($tag === null)
2915
			continue;
2916
2917
		// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
2918
		if (isset($inside['disallow_children']))
2919
			$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
2920
2921
		// Is this tag disabled?
2922
		if (isset($disabled[$tag['tag']]))
2923
		{
2924
			if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
2925
			{
2926
				$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
2927
				$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
2928
				$tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
2929
			}
2930
			elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
2931
			{
2932
				$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
2933
				$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
2934
			}
2935
			else
2936
				$tag['content'] = $tag['disabled_content'];
2937
		}
2938
2939
		// we use this a lot
2940
		$tag_strlen = strlen($tag['tag']);
2941
2942
		// The only special case is 'html', which doesn't need to close things.
2943
		if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
2944
		{
2945
			$n = count($open_tags) - 1;
2946
			while (empty($open_tags[$n]['block_level']) && $n >= 0)
2947
				$n--;
2948
2949
			// Close all the non block level tags so this tag isn't surrounded by them.
2950
			for ($i = count($open_tags) - 1; $i > $n; $i--)
2951
			{
2952
				$message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
2953
				$ot_strlen = strlen($open_tags[$i]['after']);
2954
				$pos += $ot_strlen + 2;
2955
				$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...
2956
2957
				// Trim or eat trailing stuff... see comment at the end of the big loop.
2958
				$whitespace_regex = '';
2959
				if (!empty($tag['block_level']))
2960
					$whitespace_regex .= '(&nbsp;|\s)*(<br>)?';
2961
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
2962
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2963
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
2964
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2965
2966
				array_pop($open_tags);
2967
			}
2968
		}
2969
2970
		// Can't read past the end of the message
2971
		$pos1 = min(strlen($message), $pos1);
2972
2973
		// No type means 'parsed_content'.
2974
		if (!isset($tag['type']))
2975
		{
2976
			$open_tags[] = $tag;
2977
			$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
2978
			$pos += strlen($tag['before']) - 1 + 2;
2979
		}
2980
		// Don't parse the content, just skip it.
2981
		elseif ($tag['type'] == 'unparsed_content')
2982
		{
2983
			$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
2984
			if ($pos2 === false)
2985
				continue;
2986
2987
			$data = substr($message, $pos1, $pos2 - $pos1);
2988
2989
			if (!empty($tag['block_level']) && substr($data, 0, 4) == '<br>')
2990
				$data = substr($data, 4);
2991
2992
			if (isset($tag['validate']))
2993
				$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...
2994
2995
			$code = strtr($tag['content'], array('$1' => $data));
2996
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
2997
2998
			$pos += strlen($code) - 1 + 2;
2999
			$last_pos = $pos + 1;
3000
		}
3001
		// Don't parse the content, just skip it.
3002
		elseif ($tag['type'] == 'unparsed_equals_content')
3003
		{
3004
			// The value may be quoted for some tags - check.
3005
			if (isset($tag['quoted']))
3006
			{
3007
				$quoted = substr($message, $pos1, 6) == '&quot;';
3008
				if ($tag['quoted'] != 'optional' && !$quoted)
3009
					continue;
3010
3011
				if ($quoted)
3012
					$pos1 += 6;
3013
			}
3014
			else
3015
				$quoted = false;
3016
3017
			$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...
3018
			if ($pos2 === false)
3019
				continue;
3020
3021
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3022
			if ($pos3 === false)
3023
				continue;
3024
3025
			$data = array(
3026
				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...
3027
				substr($message, $pos1, $pos2 - $pos1)
3028
			);
3029
3030
			if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '<br>')
3031
				$data[0] = substr($data[0], 4);
3032
3033
			// Validation for my parking, please!
3034
			if (isset($tag['validate']))
3035
				$tag['validate']($tag, $data, $disabled, $params);
3036
3037
			$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
3038
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3039
			$pos += strlen($code) - 1 + 2;
3040
		}
3041
		// A closed tag, with no content or value.
3042
		elseif ($tag['type'] == 'closed')
3043
		{
3044
			$pos2 = strpos($message, ']', $pos);
3045
			$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
3046
			$pos += strlen($tag['content']) - 1 + 2;
3047
		}
3048
		// This one is sorta ugly... :/.  Unfortunately, it's needed for flash.
3049
		elseif ($tag['type'] == 'unparsed_commas_content')
3050
		{
3051
			$pos2 = strpos($message, ']', $pos1);
3052
			if ($pos2 === false)
3053
				continue;
3054
3055
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3056
			if ($pos3 === false)
3057
				continue;
3058
3059
			// We want $1 to be the content, and the rest to be csv.
3060
			$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
3061
			$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
3062
3063
			if (isset($tag['validate']))
3064
				$tag['validate']($tag, $data, $disabled, $params);
3065
3066
			$code = $tag['content'];
3067
			foreach ($data as $k => $d)
3068
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3069
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3070
			$pos += strlen($code) - 1 + 2;
3071
		}
3072
		// This has parsed content, and a csv value which is unparsed.
3073
		elseif ($tag['type'] == 'unparsed_commas')
3074
		{
3075
			$pos2 = strpos($message, ']', $pos1);
3076
			if ($pos2 === false)
3077
				continue;
3078
3079
			$data = explode(',', substr($message, $pos1, $pos2 - $pos1));
3080
3081
			if (isset($tag['validate']))
3082
				$tag['validate']($tag, $data, $disabled, $params);
3083
3084
			// Fix after, for disabled code mainly.
3085
			foreach ($data as $k => $d)
3086
				$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
3087
3088
			$open_tags[] = $tag;
3089
3090
			// Replace them out, $1, $2, $3, $4, etc.
3091
			$code = $tag['before'];
3092
			foreach ($data as $k => $d)
3093
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3094
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1);
3095
			$pos += strlen($code) - 1 + 2;
3096
		}
3097
		// A tag set to a value, parsed or not.
3098
		elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
3099
		{
3100
			// The value may be quoted for some tags - check.
3101
			if (isset($tag['quoted']))
3102
			{
3103
				$quoted = substr($message, $pos1, 6) == '&quot;';
3104
				if ($tag['quoted'] != 'optional' && !$quoted)
3105
					continue;
3106
3107
				if ($quoted)
3108
					$pos1 += 6;
3109
			}
3110
			else
3111
				$quoted = false;
3112
3113
			if ($quoted)
3114
			{
3115
				$end_of_value = strpos($message, '&quot;]', $pos1);
3116
				$nested_tag = strpos($message, '=&quot;', $pos1);
3117
				if ($nested_tag && $nested_tag < $end_of_value)
3118
					// Nested tag with quoted value detected, use next end tag
3119
					$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...
3120
			}
3121
3122
			$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...
3123
			if ($pos2 === false)
3124
				continue;
3125
3126
			$data = substr($message, $pos1, $pos2 - $pos1);
3127
3128
			// Validation for my parking, please!
3129
			if (isset($tag['validate']))
3130
				$tag['validate']($tag, $data, $disabled, $params);
3131
3132
			// For parsed content, we must recurse to avoid security problems.
3133
			if ($tag['type'] != 'unparsed_equals')
3134
				$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
3135
3136
			$tag['after'] = strtr($tag['after'], array('$1' => $data));
3137
3138
			$open_tags[] = $tag;
3139
3140
			$code = strtr($tag['before'], array('$1' => $data));
3141
			$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...
3142
			$pos += strlen($code) - 1 + 2;
3143
		}
3144
3145
		// If this is block level, eat any breaks after it.
3146
		if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '<br>')
3147
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 5);
3148
3149
		// Are we trimming outside this tag?
3150
		if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
3151
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
3152
	}
3153
3154
	// Close any remaining tags.
3155
	while ($tag = array_pop($open_tags))
3156
		$message .= "\n" . $tag['after'] . "\n";
3157
3158
	// Parse the smileys within the parts where it can be done safely.
3159
	if ($smileys === true)
3160
	{
3161
		$message_parts = explode("\n", $message);
3162
		for ($i = 0, $n = count($message_parts); $i < $n; $i += 2)
3163
			parsesmileys($message_parts[$i]);
3164
3165
		$message = implode('', $message_parts);
3166
	}
3167
3168
	// No smileys, just get rid of the markers.
3169
	else
3170
		$message = strtr($message, array("\n" => ''));
3171
3172
	if ($message !== '' && $message[0] === ' ')
3173
		$message = '&nbsp;' . substr($message, 1);
3174
3175
	// Cleanup whitespace.
3176
	$message = strtr($message, array('  ' => ' &nbsp;', "\r" => '', "\n" => '<br>', '<br> ' => '<br>&nbsp;', '&#13;' => "\n"));
3177
3178
	// Allow mods access to what parse_bbc created
3179
	call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
3180
3181
	// Cache the output if it took some time...
3182
	if (isset($cache_key, $cache_t) && microtime(true) - $cache_t > 0.05)
3183
		cache_put_data($cache_key, $message, 240);
3184
3185
	// If this was a force parse revert if needed.
3186
	if (!empty($parse_tags))
3187
	{
3188
		$alltags_regex = empty($real_alltags_regex) ? '' : $real_alltags_regex;
3189
		unset($real_alltags_regex);
3190
	}
3191
	elseif (!empty($bbc_codes))
3192
		$bbc_lang_locales[$txt['lang_locale']] = $bbc_codes;
3193
3194
	return $message;
3195
}
3196
3197
/**
3198
 * Parse smileys in the passed message.
3199
 *
3200
 * The smiley parsing function which makes pretty faces appear :).
3201
 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
3202
 * These are specifically not parsed in code tags [url=mailto:[email protected]]
3203
 * Caches the smileys from the database or array in memory.
3204
 * Doesn't return anything, but rather modifies message directly.
3205
 *
3206
 * @param string &$message The message to parse smileys in
3207
 */
3208
function parsesmileys(&$message)
3209
{
3210
	global $modSettings, $txt, $user_info, $context, $smcFunc;
3211
	static $smileyPregSearch = null, $smileyPregReplacements = array();
3212
3213
	// No smiley set at all?!
3214
	if ($user_info['smiley_set'] == 'none' || trim($message) == '')
3215
		return;
3216
3217
	// Maybe a mod wants to implement an alternative method (e.g. emojis instead of images)
3218
	call_integration_hook('integrate_smileys', array(&$smileyPregSearch, &$smileyPregReplacements));
3219
3220
	// If smileyPregSearch hasn't been set, do it now.
3221
	if (empty($smileyPregSearch))
3222
	{
3223
		// Cache for longer when customized smiley codes aren't enabled
3224
		$cache_time = empty($modSettings['smiley_enable']) ? 7200 : 480;
3225
3226
		// Load the smileys in reverse order by length so they don't get parsed incorrectly.
3227
		if (($temp = cache_get_data('parsing_smileys_' . $user_info['smiley_set'], $cache_time)) == null)
3228
		{
3229
			$result = $smcFunc['db_query']('', '
3230
				SELECT s.code, f.filename, s.description
3231
				FROM {db_prefix}smileys AS s
3232
					JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
3233
				WHERE f.smiley_set = {string:smiley_set}' . (empty($modSettings['smiley_enable']) ? '
3234
					AND s.code IN ({array_string:default_codes})' : '') . '
3235
				ORDER BY LENGTH(s.code) DESC',
3236
				array(
3237
					'default_codes' => array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'),
3238
					'smiley_set' => $user_info['smiley_set'],
3239
				)
3240
			);
3241
			$smileysfrom = array();
3242
			$smileysto = array();
3243
			$smileysdescs = array();
3244
			while ($row = $smcFunc['db_fetch_assoc']($result))
3245
			{
3246
				$smileysfrom[] = $row['code'];
3247
				$smileysto[] = $smcFunc['htmlspecialchars']($row['filename']);
3248
				$smileysdescs[] = !empty($txt['icon_' . strtolower($row['description'])]) ? $txt['icon_' . strtolower($row['description'])] : $row['description'];
3249
			}
3250
			$smcFunc['db_free_result']($result);
3251
3252
			cache_put_data('parsing_smileys_' . $user_info['smiley_set'], array($smileysfrom, $smileysto, $smileysdescs), $cache_time);
3253
		}
3254
		else
3255
			list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
3256
3257
		// The non-breaking-space is a complex thing...
3258
		$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
3259
3260
		// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:[email protected]] doesn't parse the :D smiley)
3261
		$smileyPregReplacements = array();
3262
		$searchParts = array();
3263
		$smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
3264
3265
		for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
3266
		{
3267
			$specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES);
3268
			$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">';
3269
3270
			$smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
3271
3272
			$searchParts[] = $smileysfrom[$i];
3273
			if ($smileysfrom[$i] != $specialChars)
3274
			{
3275
				$smileyPregReplacements[$specialChars] = $smileyCode;
3276
				$searchParts[] = $specialChars;
3277
3278
				// Some 2.0 hex htmlchars are in there as 3 digits; allow for finding leading 0 or not
3279
				$specialChars2 = preg_replace('/&#(\d{2});/', '&#0$1;', $specialChars);
3280
				if ($specialChars2 != $specialChars)
3281
				{
3282
					$smileyPregReplacements[$specialChars2] = $smileyCode;
3283
					$searchParts[] = $specialChars2;
3284
				}
3285
			}
3286
		}
3287
3288
		$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

3288
		$smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?<![a-zA-Z0-9])\(|^)(' . /** @scrutinizer ignore-type */ build_regex($searchParts, '~') . ')(?=[^[:alpha:]0-9]|$)~' . ($context['utf8'] ? 'u' : '');
Loading history...
3289
	}
3290
3291
	// Replace away!
3292
	$message = preg_replace_callback($smileyPregSearch, function($matches) use ($smileyPregReplacements)
3293
		{
3294
			return $smileyPregReplacements[$matches[1]];
3295
		}, $message);
3296
}
3297
3298
/**
3299
 * Highlight any code.
3300
 *
3301
 * Uses PHP's highlight_string() to highlight PHP syntax
3302
 * does special handling to keep the tabs in the code available.
3303
 * used to parse PHP code from inside [code] and [php] tags.
3304
 *
3305
 * @param string $code The code
3306
 * @return string The code with highlighted HTML.
3307
 */
3308
function highlight_php_code($code)
3309
{
3310
	// Remove special characters.
3311
	$code = un_htmlspecialchars(strtr($code, array('<br />' => "\n", '<br>' => "\n", "\t" => 'SMF_TAB();', '&#91;' => '[')));
3312
3313
	$oldlevel = error_reporting(0);
3314
3315
	$buffer = str_replace(array("\n", "\r"), '', @highlight_string($code, true));
3316
3317
	error_reporting($oldlevel);
3318
3319
	// Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
3320
	$buffer = preg_replace('~SMF_TAB(?:</(?:font|span)><(?:font color|span style)="[^"]*?">)?\\(\\);~', '<pre style="display: inline;">' . "\t" . '</pre>', $buffer);
3321
3322
	return strtr($buffer, array('\'' => '&#039;', '<code>' => '', '</code>' => ''));
3323
}
3324
3325
/**
3326
 * Gets the appropriate URL to use for images (or whatever) when using SSL
3327
 *
3328
 * The returned URL may or may not be a proxied URL, depending on the situation.
3329
 * Mods can implement alternative proxies using the 'integrate_proxy' hook.
3330
 *
3331
 * @param string $url The original URL of the requested resource
3332
 * @return string The URL to use
3333
 */
3334
function get_proxied_url($url)
3335
{
3336
	global $boardurl, $image_proxy_enabled, $image_proxy_secret, $user_info;
3337
3338
	// Only use the proxy if enabled, and never for robots
3339
	if (empty($image_proxy_enabled) || !empty($user_info['possibly_robot']))
3340
		return $url;
3341
3342
	$parsedurl = parse_url($url);
3343
3344
	// Don't bother with HTTPS URLs, schemeless URLs, or obviously invalid URLs
3345
	if (empty($parsedurl['scheme']) || empty($parsedurl['host']) || empty($parsedurl['path']) || $parsedurl['scheme'] === 'https')
3346
		return $url;
3347
3348
	// We don't need to proxy our own resources
3349
	if ($parsedurl['host'] === parse_url($boardurl, PHP_URL_HOST))
3350
		return strtr($url, array('http://' => 'https://'));
3351
3352
	// By default, use SMF's own image proxy script
3353
	$proxied_url = strtr($boardurl, array('http://' => 'https://')) . '/proxy.php?request=' . urlencode($url) . '&hash=' . hash_hmac('sha1', $url, $image_proxy_secret);
3354
3355
	// Allow mods to easily implement an alternative proxy
3356
	// MOD AUTHORS: To add settings UI for your proxy, use the integrate_general_settings hook.
3357
	call_integration_hook('integrate_proxy', array($url, &$proxied_url));
3358
3359
	return $proxied_url;
3360
}
3361
3362
/**
3363
 * Make sure the browser doesn't come back and repost the form data.
3364
 * Should be used whenever anything is posted.
3365
 *
3366
 * @param string $setLocation The URL to redirect them to
3367
 * @param bool $refresh Whether to use a meta refresh instead
3368
 * @param bool $permanent Whether to send a 301 Moved Permanently instead of a 302 Moved Temporarily
3369
 */
3370
function redirectexit($setLocation = '', $refresh = false, $permanent = false)
3371
{
3372
	global $scripturl, $context, $modSettings, $db_show_debug, $db_cache;
3373
3374
	// In case we have mail to send, better do that - as obExit doesn't always quite make it...
3375
	if (!empty($context['flush_mail']))
3376
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
3377
		AddMailQueue(true);
3378
3379
	$add = preg_match('~^(ftp|http)[s]?://~', $setLocation) == 0 && substr($setLocation, 0, 6) != 'about:';
3380
3381
	if ($add)
3382
		$setLocation = $scripturl . ($setLocation != '' ? '?' . $setLocation : '');
3383
3384
	// Put the session ID in.
3385
	if (defined('SID') && SID != '')
3386
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', $scripturl . '?' . SID . ';', $setLocation);
3387
	// Keep that debug in their for template debugging!
3388
	elseif (isset($_GET['debug']))
3389
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);
3390
3391
	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'])))
3392
	{
3393
		if (defined('SID') && SID != '')
3394
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '~') . '\?(?:' . SID . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~',
3395
				function($m) use ($scripturl)
3396
				{
3397
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . SID . (isset($m[2]) ? "$m[2]" : "");
3398
				}, $setLocation);
3399
		else
3400
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '~') . '\?((?:board|topic)=[^#"]+?)(#[^"]*?)?$~',
3401
				function($m) use ($scripturl)
3402
				{
3403
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html' . (isset($m[2]) ? "$m[2]" : "");
3404
				}, $setLocation);
3405
	}
3406
3407
	// Maybe integrations want to change where we are heading?
3408
	call_integration_hook('integrate_redirect', array(&$setLocation, &$refresh, &$permanent));
3409
3410
	// Set the header.
3411
	header('location: ' . str_replace(' ', '%20', $setLocation), true, $permanent ? 301 : 302);
3412
3413
	// Debugging.
3414
	if (isset($db_show_debug) && $db_show_debug === true)
3415
		$_SESSION['debug_redirect'] = $db_cache;
3416
3417
	obExit(false);
3418
}
3419
3420
/**
3421
 * Ends execution.  Takes care of template loading and remembering the previous URL.
3422
 *
3423
 * @param bool $header Whether to do the header
3424
 * @param bool $do_footer Whether to do the footer
3425
 * @param bool $from_index Whether we're coming from the board index
3426
 * @param bool $from_fatal_error Whether we're coming from a fatal error
3427
 */
3428
function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false)
3429
{
3430
	global $context, $settings, $modSettings, $txt, $smcFunc, $should_log;
3431
	static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
3432
3433
	// Attempt to prevent a recursive loop.
3434
	++$level;
3435
	if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
3436
		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...
3437
	if ($from_fatal_error)
3438
		$has_fatal_error = true;
3439
3440
	// Clear out the stat cache.
3441
	if (function_exists('trackStats'))
3442
		trackStats();
3443
3444
	// If we have mail to send, send it.
3445
	if (function_exists('AddMailQueue') && !empty($context['flush_mail']))
3446
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
3447
		AddMailQueue(true);
3448
3449
	$do_header = $header === null ? !$header_done : $header;
3450
	if ($do_footer === null)
3451
		$do_footer = $do_header;
3452
3453
	// Has the template/header been done yet?
3454
	if ($do_header)
3455
	{
3456
		// Was the page title set last minute? Also update the HTML safe one.
3457
		if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
3458
			$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3459
3460
		// Start up the session URL fixer.
3461
		ob_start('ob_sessrewrite');
3462
3463
		if (!empty($settings['output_buffers']) && is_string($settings['output_buffers']))
3464
			$buffers = explode(',', $settings['output_buffers']);
3465
		elseif (!empty($settings['output_buffers']))
3466
			$buffers = $settings['output_buffers'];
3467
		else
3468
			$buffers = array();
3469
3470
		if (isset($modSettings['integrate_buffer']))
3471
			$buffers = array_merge(explode(',', $modSettings['integrate_buffer']), $buffers);
3472
3473
		if (!empty($buffers))
3474
			foreach ($buffers as $function)
3475
			{
3476
				$call = call_helper($function, true);
3477
3478
				// Is it valid?
3479
				if (!empty($call))
3480
					ob_start($call);
0 ignored issues
show
Bug introduced by
It seems like $call can also be of type boolean; however, parameter $output_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

3480
					ob_start(/** @scrutinizer ignore-type */ $call);
Loading history...
3481
			}
3482
3483
		// Display the screen in the logical order.
3484
		template_header();
3485
		$header_done = true;
3486
	}
3487
	if ($do_footer)
3488
	{
3489
		loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main');
3490
3491
		// Anything special to put out?
3492
		if (!empty($context['insert_after_template']) && !isset($_REQUEST['xml']))
3493
			echo $context['insert_after_template'];
3494
3495
		// Just so we don't get caught in an endless loop of errors from the footer...
3496
		if (!$footer_done)
3497
		{
3498
			$footer_done = true;
3499
			template_footer();
3500
3501
			// (since this is just debugging... it's okay that it's after </html>.)
3502
			if (!isset($_REQUEST['xml']))
3503
				displayDebug();
3504
		}
3505
	}
3506
3507
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
3508
	if ($should_log)
3509
		$_SESSION['old_url'] = $_SERVER['REQUEST_URL'];
3510
3511
	// For session check verification.... don't switch browsers...
3512
	$_SESSION['USER_AGENT'] = empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT'];
3513
3514
	// Hand off the output to the portal, etc. we're integrated with.
3515
	call_integration_hook('integrate_exit', array($do_footer));
3516
3517
	// Don't exit if we're coming from index.php; that will pass through normally.
3518
	if (!$from_index)
3519
		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...
3520
}
3521
3522
/**
3523
 * Get the size of a specified image with better error handling.
3524
 *
3525
 * @todo see if it's better in Subs-Graphics, but one step at the time.
3526
 * Uses getimagesize() to determine the size of a file.
3527
 * Attempts to connect to the server first so it won't time out.
3528
 *
3529
 * @param string $url The URL of the image
3530
 * @return array|false The image size as array (width, height), or false on failure
3531
 */
3532
function url_image_size($url)
3533
{
3534
	global $sourcedir;
3535
3536
	// Make sure it is a proper URL.
3537
	$url = str_replace(' ', '%20', $url);
3538
3539
	// Can we pull this from the cache... please please?
3540
	if (($temp = cache_get_data('url_image_size-' . md5($url), 240)) !== null)
3541
		return $temp;
3542
	$t = microtime(true);
3543
3544
	// Get the host to pester...
3545
	preg_match('~^\w+://(.+?)/(.*)$~', $url, $match);
3546
3547
	// Can't figure it out, just try the image size.
3548
	if ($url == '' || $url == 'http://' || $url == 'https://')
3549
	{
3550
		return false;
3551
	}
3552
	elseif (!isset($match[1]))
3553
	{
3554
		$size = @getimagesize($url);
3555
	}
3556
	else
3557
	{
3558
		// Try to connect to the server... give it half a second.
3559
		$temp = 0;
3560
		$fp = @fsockopen($match[1], 80, $temp, $temp, 0.5);
3561
3562
		// Successful?  Continue...
3563
		if ($fp != false)
3564
		{
3565
			// Send the HEAD request (since we don't have to worry about chunked, HTTP/1.1 is fine here.)
3566
			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");
3567
3568
			// Read in the HTTP/1.1 or whatever.
3569
			$test = substr(fgets($fp, 11), -1);
3570
			fclose($fp);
3571
3572
			// See if it returned a 404/403 or something.
3573
			if ($test < 4)
3574
			{
3575
				$size = @getimagesize($url);
3576
3577
				// This probably means allow_url_fopen is off, let's try GD.
3578
				if ($size === false && function_exists('imagecreatefromstring'))
3579
				{
3580
					// It's going to hate us for doing this, but another request...
3581
					$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

3581
					$image = @imagecreatefromstring(/** @scrutinizer ignore-type */ fetch_web_data($url));
Loading history...
3582
					if ($image !== false)
3583
					{
3584
						$size = array(imagesx($image), imagesy($image));
3585
						imagedestroy($image);
3586
					}
3587
				}
3588
			}
3589
		}
3590
	}
3591
3592
	// If we didn't get it, we failed.
3593
	if (!isset($size))
3594
		$size = false;
3595
3596
	// If this took a long time, we may never have to do it again, but then again we might...
3597
	if (microtime(true) - $t > 0.8)
3598
		cache_put_data('url_image_size-' . md5($url), $size, 240);
3599
3600
	// Didn't work.
3601
	return $size;
3602
}
3603
3604
/**
3605
 * Sets up the basic theme context stuff.
3606
 *
3607
 * @param bool $forceload Whether to load the theme even if it's already loaded
3608
 */
3609
function setupThemeContext($forceload = false)
3610
{
3611
	global $modSettings, $user_info, $scripturl, $context, $settings, $options, $txt, $maintenance;
3612
	global $smcFunc;
3613
	static $loaded = false;
3614
3615
	// Under SSI this function can be called more then once.  That can cause some problems.
3616
	//   So only run the function once unless we are forced to run it again.
3617
	if ($loaded && !$forceload)
3618
		return;
3619
3620
	$loaded = true;
3621
3622
	$context['in_maintenance'] = !empty($maintenance);
3623
	$context['current_time'] = timeformat(time(), false);
3624
	$context['current_action'] = isset($_GET['action']) ? $smcFunc['htmlspecialchars']($_GET['action']) : '';
3625
	$context['random_news_line'] = array();
3626
3627
	// Get some news...
3628
	$context['news_lines'] = array_filter(explode("\n", str_replace("\r", '', trim(addslashes($modSettings['news'])))));
3629
	for ($i = 0, $n = count($context['news_lines']); $i < $n; $i++)
3630
	{
3631
		if (trim($context['news_lines'][$i]) == '')
3632
			continue;
3633
3634
		// Clean it up for presentation ;).
3635
		$context['news_lines'][$i] = parse_bbc(stripslashes(trim($context['news_lines'][$i])), true, 'news' . $i);
3636
	}
3637
3638
	if (!empty($context['news_lines']) && (!empty($modSettings['allow_guestAccess']) || $context['user']['is_logged']))
3639
		$context['random_news_line'] = $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)];
3640
3641
	if (!$user_info['is_guest'])
3642
	{
3643
		$context['user']['messages'] = &$user_info['messages'];
3644
		$context['user']['unread_messages'] = &$user_info['unread_messages'];
3645
		$context['user']['alerts'] = &$user_info['alerts'];
3646
3647
		// Personal message popup...
3648
		if ($user_info['unread_messages'] > (isset($_SESSION['unread_messages']) ? $_SESSION['unread_messages'] : 0))
3649
			$context['user']['popup_messages'] = true;
3650
		else
3651
			$context['user']['popup_messages'] = false;
3652
		$_SESSION['unread_messages'] = $user_info['unread_messages'];
3653
3654
		if (allowedTo('moderate_forum'))
3655
			$context['unapproved_members'] = !empty($modSettings['unapprovedMembers']) ? $modSettings['unapprovedMembers'] : 0;
3656
3657
		$context['user']['avatar'] = array();
3658
3659
		// Check for gravatar first since we might be forcing them...
3660
		if (($modSettings['gravatarEnabled'] && substr($user_info['avatar']['url'], 0, 11) == 'gravatar://') || !empty($modSettings['gravatarOverride']))
3661
		{
3662
			if (!empty($modSettings['gravatarAllowExtraEmail']) && stristr($user_info['avatar']['url'], 'gravatar://') && strlen($user_info['avatar']['url']) > 11)
3663
				$context['user']['avatar']['href'] = get_gravatar_url($smcFunc['substr']($user_info['avatar']['url'], 11));
3664
			else
3665
				$context['user']['avatar']['href'] = get_gravatar_url($user_info['email']);
3666
		}
3667
		// Uploaded?
3668
		elseif ($user_info['avatar']['url'] == '' && !empty($user_info['avatar']['id_attach']))
3669
			$context['user']['avatar']['href'] = $user_info['avatar']['custom_dir'] ? $modSettings['custom_avatar_url'] . '/' . $user_info['avatar']['filename'] : $scripturl . '?action=dlattach;attach=' . $user_info['avatar']['id_attach'] . ';type=avatar';
3670
		// Full URL?
3671
		elseif (strpos($user_info['avatar']['url'], 'http://') === 0 || strpos($user_info['avatar']['url'], 'https://') === 0)
3672
			$context['user']['avatar']['href'] = $user_info['avatar']['url'];
3673
		// Otherwise we assume it's server stored.
3674
		elseif ($user_info['avatar']['url'] != '')
3675
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/' . $smcFunc['htmlspecialchars']($user_info['avatar']['url']);
3676
		// No avatar at all? Fine, we have a big fat default avatar ;)
3677
		else
3678
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/default.png';
3679
3680
		if (!empty($context['user']['avatar']))
3681
			$context['user']['avatar']['image'] = '<img src="' . $context['user']['avatar']['href'] . '" alt="" class="avatar">';
3682
3683
		// Figure out how long they've been logged in.
3684
		$context['user']['total_time_logged_in'] = array(
3685
			'days' => floor($user_info['total_time_logged_in'] / 86400),
3686
			'hours' => floor(($user_info['total_time_logged_in'] % 86400) / 3600),
3687
			'minutes' => floor(($user_info['total_time_logged_in'] % 3600) / 60)
3688
		);
3689
	}
3690
	else
3691
	{
3692
		$context['user']['messages'] = 0;
3693
		$context['user']['unread_messages'] = 0;
3694
		$context['user']['avatar'] = array();
3695
		$context['user']['total_time_logged_in'] = array('days' => 0, 'hours' => 0, 'minutes' => 0);
3696
		$context['user']['popup_messages'] = false;
3697
3698
		if (!empty($modSettings['registration_method']) && $modSettings['registration_method'] == 1)
3699
			$txt['welcome_guest'] .= sprintf($txt['welcome_guest_activate'], $scripturl);
3700
3701
		// If we've upgraded recently, go easy on the passwords.
3702
		if (!empty($modSettings['disableHashTime']) && ($modSettings['disableHashTime'] == 1 || time() < $modSettings['disableHashTime']))
3703
			$context['disable_login_hashing'] = true;
3704
	}
3705
3706
	// Setup the main menu items.
3707
	setupMenuContext();
3708
3709
	// This is here because old index templates might still use it.
3710
	$context['show_news'] = !empty($settings['enable_news']);
3711
3712
	// This is done to allow theme authors to customize it as they want.
3713
	$context['show_pm_popup'] = $context['user']['popup_messages'] && !empty($options['popup_messages']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != 'pm');
3714
3715
	// 2.1+: Add the PM popup here instead. Theme authors can still override it simply by editing/removing the 'fPmPopup' in the array.
3716
	if ($context['show_pm_popup'])
3717
		addInlineJavaScript('
3718
		jQuery(document).ready(function($) {
3719
			new smc_Popup({
3720
				heading: ' . JavaScriptEscape($txt['show_personal_messages_heading']) . ',
3721
				content: ' . JavaScriptEscape(sprintf($txt['show_personal_messages'], $context['user']['unread_messages'], $scripturl . '?action=pm')) . ',
3722
				icon_class: \'main_icons mail_new\'
3723
			});
3724
		});');
3725
3726
	// Add a generic "Are you sure?" confirmation message.
3727
	addInlineJavaScript('
3728
	var smf_you_sure =' . JavaScriptEscape($txt['quickmod_confirm']) . ';');
3729
3730
	// Now add the capping code for avatars.
3731
	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')
3732
		addInlineCss('
3733
	img.avatar { max-width: ' . $modSettings['avatar_max_width_external'] . 'px; max-height: ' . $modSettings['avatar_max_height_external'] . 'px; }');
3734
3735
	// Add max image limits
3736
	if (!empty($modSettings['max_image_width']))
3737
		addInlineCss('
3738
	.postarea .bbc_img { max-width: ' . $modSettings['max_image_width'] . 'px; }');
3739
3740
	if (!empty($modSettings['max_image_height']))
3741
		addInlineCss('
3742
	.postarea .bbc_img { max-height: ' . $modSettings['max_image_height'] . 'px; }');
3743
3744
	// This looks weird, but it's because BoardIndex.php references the variable.
3745
	$context['common_stats']['latest_member'] = array(
3746
		'id' => $modSettings['latestMember'],
3747
		'name' => $modSettings['latestRealName'],
3748
		'href' => $scripturl . '?action=profile;u=' . $modSettings['latestMember'],
3749
		'link' => '<a href="' . $scripturl . '?action=profile;u=' . $modSettings['latestMember'] . '">' . $modSettings['latestRealName'] . '</a>',
3750
	);
3751
	$context['common_stats'] = array(
3752
		'total_posts' => comma_format($modSettings['totalMessages']),
3753
		'total_topics' => comma_format($modSettings['totalTopics']),
3754
		'total_members' => comma_format($modSettings['totalMembers']),
3755
		'latest_member' => $context['common_stats']['latest_member'],
3756
	);
3757
	$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']);
3758
3759
	if (empty($settings['theme_version']))
3760
		addJavaScriptVar('smf_scripturl', $scripturl);
3761
3762
	if (!isset($context['page_title']))
3763
		$context['page_title'] = '';
3764
3765
	// Set some specific vars.
3766
	$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3767
	$context['meta_keywords'] = !empty($modSettings['meta_keywords']) ? $smcFunc['htmlspecialchars']($modSettings['meta_keywords']) : '';
3768
3769
	// Content related meta tags, including Open Graph
3770
	$context['meta_tags'][] = array('property' => 'og:site_name', 'content' => $context['forum_name']);
3771
	$context['meta_tags'][] = array('property' => 'og:title', 'content' => $context['page_title_html_safe']);
3772
3773
	if (!empty($context['meta_keywords']))
3774
		$context['meta_tags'][] = array('name' => 'keywords', 'content' => $context['meta_keywords']);
3775
3776
	if (!empty($context['canonical_url']))
3777
		$context['meta_tags'][] = array('property' => 'og:url', 'content' => $context['canonical_url']);
3778
3779
	if (!empty($settings['og_image']))
3780
		$context['meta_tags'][] = array('property' => 'og:image', 'content' => $settings['og_image']);
3781
3782
	if (!empty($context['meta_description']))
3783
	{
3784
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['meta_description']);
3785
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['meta_description']);
3786
	}
3787
	else
3788
	{
3789
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['page_title_html_safe']);
3790
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['page_title_html_safe']);
3791
	}
3792
3793
	call_integration_hook('integrate_theme_context');
3794
}
3795
3796
/**
3797
 * Helper function to set the system memory to a needed value
3798
 * - If the needed memory is greater than current, will attempt to get more
3799
 * - if in_use is set to true, will also try to take the current memory usage in to account
3800
 *
3801
 * @param string $needed The amount of memory to request, if needed, like 256M
3802
 * @param bool $in_use Set to true to account for current memory usage of the script
3803
 * @return boolean True if we have at least the needed memory
3804
 */
3805
function setMemoryLimit($needed, $in_use = false)
3806
{
3807
	// everything in bytes
3808
	$memory_current = memoryReturnBytes(ini_get('memory_limit'));
3809
	$memory_needed = memoryReturnBytes($needed);
3810
3811
	// should we account for how much is currently being used?
3812
	if ($in_use)
3813
		$memory_needed += function_exists('memory_get_usage') ? memory_get_usage() : (2 * 1048576);
3814
3815
	// if more is needed, request it
3816
	if ($memory_current < $memory_needed)
3817
	{
3818
		@ini_set('memory_limit', ceil($memory_needed / 1048576) . 'M');
3819
		$memory_current = memoryReturnBytes(ini_get('memory_limit'));
3820
	}
3821
3822
	$memory_current = max($memory_current, memoryReturnBytes(get_cfg_var('memory_limit')));
3823
3824
	// return success or not
3825
	return (bool) ($memory_current >= $memory_needed);
3826
}
3827
3828
/**
3829
 * Helper function to convert memory string settings to bytes
3830
 *
3831
 * @param string $val The byte string, like 256M or 1G
3832
 * @return integer The string converted to a proper integer in bytes
3833
 */
3834
function memoryReturnBytes($val)
3835
{
3836
	if (is_integer($val))
0 ignored issues
show
introduced by
The condition is_integer($val) is always false.
Loading history...
3837
		return $val;
3838
3839
	// Separate the number from the designator
3840
	$val = trim($val);
3841
	$num = intval(substr($val, 0, strlen($val) - 1));
3842
	$last = strtolower(substr($val, -1));
3843
3844
	// convert to bytes
3845
	switch ($last)
3846
	{
3847
		case 'g':
3848
			$num *= 1024;
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
3849
		case 'm':
3850
			$num *= 1024;
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
3851
		case 'k':
3852
			$num *= 1024;
3853
	}
3854
	return $num;
3855
}
3856
3857
/**
3858
 * The header template
3859
 */
3860
function template_header()
3861
{
3862
	global $txt, $modSettings, $context, $user_info, $boarddir, $cachedir, $cache_enable;
3863
3864
	setupThemeContext();
3865
3866
	// Print stuff to prevent caching of pages (except on attachment errors, etc.)
3867
	if (empty($context['no_last_modified']))
3868
	{
3869
		header('expires: Mon, 26 Jul 1997 05:00:00 GMT');
3870
		header('last-modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
3871
3872
		// Are we debugging the template/html content?
3873
		if (!isset($_REQUEST['xml']) && isset($_GET['debug']) && !isBrowser('ie'))
3874
			header('content-type: application/xhtml+xml');
3875
		elseif (!isset($_REQUEST['xml']))
3876
			header('content-type: text/html; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
3877
	}
3878
3879
	header('content-type: text/' . (isset($_REQUEST['xml']) ? 'xml' : 'html') . '; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
3880
3881
	// We need to splice this in after the body layer, or after the main layer for older stuff.
3882
	if ($context['in_maintenance'] && $context['user']['is_admin'])
3883
	{
3884
		$position = array_search('body', $context['template_layers']);
3885
		if ($position === false)
3886
			$position = array_search('main', $context['template_layers']);
3887
3888
		if ($position !== false)
3889
		{
3890
			$before = array_slice($context['template_layers'], 0, $position + 1);
3891
			$after = array_slice($context['template_layers'], $position + 1);
3892
			$context['template_layers'] = array_merge($before, array('maint_warning'), $after);
3893
		}
3894
	}
3895
3896
	$checked_securityFiles = false;
3897
	$showed_banned = false;
3898
	foreach ($context['template_layers'] as $layer)
3899
	{
3900
		loadSubTemplate($layer . '_above', true);
3901
3902
		// May seem contrived, but this is done in case the body and main layer aren't there...
3903
		if (in_array($layer, array('body', 'main')) && allowedTo('admin_forum') && !$user_info['is_guest'] && !$checked_securityFiles)
3904
		{
3905
			$checked_securityFiles = true;
3906
3907
			$securityFiles = array('install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~');
3908
3909
			// Add your own files.
3910
			call_integration_hook('integrate_security_files', array(&$securityFiles));
3911
3912
			foreach ($securityFiles as $i => $securityFile)
3913
			{
3914
				if (!file_exists($boarddir . '/' . $securityFile))
3915
					unset($securityFiles[$i]);
3916
			}
3917
3918
			// We are already checking so many files...just few more doesn't make any difference! :P
3919
			if (!empty($modSettings['currentAttachmentUploadDir']))
3920
				$path = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
3921
3922
			else
3923
				$path = $modSettings['attachmentUploadDir'];
3924
3925
			secureDirectory($path, true);
3926
			secureDirectory($cachedir);
3927
3928
			// If agreement is enabled, at least the english version shall exists
3929
			if ($modSettings['requireAgreement'])
3930
				$agreement = !file_exists($boarddir . '/agreement.txt');
3931
3932
			if (!empty($securityFiles) || (!empty($cache_enable) && !is_writable($cachedir)) || !empty($agreement))
3933
			{
3934
				echo '
3935
		<div class="errorbox">
3936
			<p class="alert">!!</p>
3937
			<h3>', empty($securityFiles) ? $txt['generic_warning'] : $txt['security_risk'], '</h3>
3938
			<p>';
3939
3940
				foreach ($securityFiles as $securityFile)
3941
				{
3942
					echo '
3943
				', $txt['not_removed'], '<strong>', $securityFile, '</strong>!<br>';
3944
3945
					if ($securityFile == 'Settings.php~' || $securityFile == 'Settings_bak.php~')
3946
						echo '
3947
				', sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)), '<br>';
3948
				}
3949
3950
				if (!empty($cache_enable) && !is_writable($cachedir))
3951
					echo '
3952
				<strong>', $txt['cache_writable'], '</strong><br>';
3953
3954
				if (!empty($agreement))
3955
					echo '
3956
				<strong>', $txt['agreement_missing'], '</strong><br>';
3957
3958
				echo '
3959
			</p>
3960
		</div>';
3961
			}
3962
		}
3963
		// If the user is banned from posting inform them of it.
3964
		elseif (in_array($layer, array('main', 'body')) && isset($_SESSION['ban']['cannot_post']) && !$showed_banned)
3965
		{
3966
			$showed_banned = true;
3967
			echo '
3968
				<div class="windowbg alert" style="margin: 2ex; padding: 2ex; border: 2px dashed red;">
3969
					', sprintf($txt['you_are_post_banned'], $user_info['is_guest'] ? $txt['guest_title'] : $user_info['name']);
3970
3971
			if (!empty($_SESSION['ban']['cannot_post']['reason']))
3972
				echo '
3973
					<div style="padding-left: 4ex; padding-top: 1ex;">', $_SESSION['ban']['cannot_post']['reason'], '</div>';
3974
3975
			if (!empty($_SESSION['ban']['expire_time']))
3976
				echo '
3977
					<div>', sprintf($txt['your_ban_expires'], timeformat($_SESSION['ban']['expire_time'], false)), '</div>';
3978
			else
3979
				echo '
3980
					<div>', $txt['your_ban_expires_never'], '</div>';
3981
3982
			echo '
3983
				</div>';
3984
		}
3985
	}
3986
}
3987
3988
/**
3989
 * Show the copyright.
3990
 */
3991
function theme_copyright()
3992
{
3993
	global $forum_copyright, $scripturl;
3994
3995
	// Don't display copyright for things like SSI.
3996
	if (SMF !== 1)
0 ignored issues
show
introduced by
The condition SMF !== 1 is always true.
Loading history...
3997
		return;
3998
3999
	// Put in the version...
4000
	printf($forum_copyright, SMF_FULL_VERSION, SMF_SOFTWARE_YEAR, $scripturl);
4001
}
4002
4003
/**
4004
 * The template footer
4005
 */
4006
function template_footer()
4007
{
4008
	global $context, $modSettings, $db_count;
4009
4010
	// Show the load time?  (only makes sense for the footer.)
4011
	$context['show_load_time'] = !empty($modSettings['timeLoadPageEnable']);
4012
	$context['load_time'] = round(microtime(true) - TIME_START, 3);
4013
	$context['load_queries'] = $db_count;
4014
4015
	if (!empty($context['template_layers']) && is_array($context['template_layers']))
4016
		foreach (array_reverse($context['template_layers']) as $layer)
4017
			loadSubTemplate($layer . '_below', true);
4018
}
4019
4020
/**
4021
 * Output the Javascript files
4022
 * 	- tabbing in this function is to make the HTML source look good and proper
4023
 *  - if deferred is set function will output all JS set to load at page end
4024
 *
4025
 * @param bool $do_deferred If true will only output the deferred JS (the stuff that goes right before the closing body tag)
4026
 */
4027
function template_javascript($do_deferred = false)
4028
{
4029
	global $context, $modSettings, $settings;
4030
4031
	// Use this hook to minify/optimize Javascript files and vars
4032
	call_integration_hook('integrate_pre_javascript_output', array(&$do_deferred));
4033
4034
	$toMinify = array(
4035
		'standard' => array(),
4036
		'defer' => array(),
4037
		'async' => array(),
4038
	);
4039
4040
	// Ouput the declared Javascript variables.
4041
	if (!empty($context['javascript_vars']) && !$do_deferred)
4042
	{
4043
		echo '
4044
	<script>';
4045
4046
		foreach ($context['javascript_vars'] as $key => $value)
4047
		{
4048
			if (empty($value))
4049
			{
4050
				echo '
4051
		var ', $key, ';';
4052
			}
4053
			else
4054
			{
4055
				echo '
4056
		var ', $key, ' = ', $value, ';';
4057
			}
4058
		}
4059
4060
		echo '
4061
	</script>';
4062
	}
4063
4064
	// In the dark days before HTML5, deferred JS files needed to be loaded at the end of the body.
4065
	// Now we load them in the head and use 'async' and/or 'defer' attributes. Much better performance.
4066
	if (!$do_deferred)
4067
	{
4068
		// While we have JavaScript files to place in the template.
4069
		foreach ($context['javascript_files'] as $id => $js_file)
4070
		{
4071
			// Last minute call! allow theme authors to disable single files.
4072
			if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
4073
				continue;
4074
4075
			// By default files don't get minimized unless the file explicitly says so!
4076
			if (!empty($js_file['options']['minimize']) && !empty($modSettings['minimize_files']))
4077
			{
4078
				if (!empty($js_file['options']['async']))
4079
					$toMinify['async'][] = $js_file;
4080
4081
				elseif (!empty($js_file['options']['defer']))
4082
					$toMinify['defer'][] = $js_file;
4083
4084
				else
4085
					$toMinify['standard'][] = $js_file;
4086
4087
				// Grab a random seed.
4088
				if (!isset($minSeed) && isset($js_file['options']['seed']))
4089
					$minSeed = $js_file['options']['seed'];
4090
			}
4091
4092
			else
4093
			{
4094
				echo '
4095
	<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' : '';
4096
4097
				if (!empty($js_file['options']['attributes']))
4098
					foreach ($js_file['options']['attributes'] as $key => $value)
4099
					{
4100
						if (is_bool($value))
4101
							echo !empty($value) ? ' ' . $key : '';
4102
4103
						else
4104
							echo ' ', $key, '="', $value, '"';
4105
					}
4106
4107
				echo '></script>';
4108
			}
4109
		}
4110
4111
		foreach ($toMinify as $js_files)
4112
		{
4113
			if (!empty($js_files))
4114
			{
4115
				$result = custMinify($js_files, 'js');
4116
4117
				$minSuccessful = array_keys($result) === array('smf_minified');
4118
4119
				foreach ($result as $minFile)
4120
					echo '
4121
	<script src="', $minFile['fileUrl'], $minSuccessful && isset($minSeed) ? $minSeed : '', '"', !empty($minFile['options']['async']) ? ' async' : '', !empty($minFile['options']['defer']) ? ' defer' : '', '></script>';
4122
			}
4123
		}
4124
	}
4125
4126
	// Inline JavaScript - Actually useful some times!
4127
	if (!empty($context['javascript_inline']))
4128
	{
4129
		if (!empty($context['javascript_inline']['defer']) && $do_deferred)
4130
		{
4131
			echo '
4132
<script>
4133
window.addEventListener("DOMContentLoaded", function() {';
4134
4135
			foreach ($context['javascript_inline']['defer'] as $js_code)
4136
				echo $js_code;
4137
4138
			echo '
4139
});
4140
</script>';
4141
		}
4142
4143
		if (!empty($context['javascript_inline']['standard']) && !$do_deferred)
4144
		{
4145
			echo '
4146
	<script>';
4147
4148
			foreach ($context['javascript_inline']['standard'] as $js_code)
4149
				echo $js_code;
4150
4151
			echo '
4152
	</script>';
4153
		}
4154
	}
4155
}
4156
4157
/**
4158
 * Output the CSS files
4159
 */
4160
function template_css()
4161
{
4162
	global $context, $db_show_debug, $boardurl, $settings, $modSettings;
4163
4164
	// Use this hook to minify/optimize CSS files
4165
	call_integration_hook('integrate_pre_css_output');
4166
4167
	$toMinify = array();
4168
	$normal = array();
4169
4170
	uasort($context['css_files'], function ($a, $b)
4171
	{
4172
		return $a['options']['order_pos'] < $b['options']['order_pos'] ? -1 : ($a['options']['order_pos'] > $b['options']['order_pos'] ? 1 : 0);
4173
	});
4174
	foreach ($context['css_files'] as $id => $file)
4175
	{
4176
		// Last minute call! allow theme authors to disable single files.
4177
		if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
4178
			continue;
4179
4180
		// Files are minimized unless they explicitly opt out.
4181
		if (!isset($file['options']['minimize']))
4182
			$file['options']['minimize'] = true;
4183
4184
		if (!empty($file['options']['minimize']) && !empty($modSettings['minimize_files']) && !isset($_REQUEST['normalcss']))
4185
		{
4186
			$toMinify[] = $file;
4187
4188
			// Grab a random seed.
4189
			if (!isset($minSeed) && isset($file['options']['seed']))
4190
				$minSeed = $file['options']['seed'];
4191
		}
4192
		else
4193
			$normal[] = array(
4194
				'url' => $file['fileUrl'] . (isset($file['options']['seed']) ? $file['options']['seed'] : ''),
4195
				'attributes' => !empty($file['options']['attributes']) ? $file['options']['attributes'] : array()
4196
			);
4197
	}
4198
4199
	if (!empty($toMinify))
4200
	{
4201
		$result = custMinify($toMinify, 'css');
4202
4203
		$minSuccessful = array_keys($result) === array('smf_minified');
4204
4205
		foreach ($result as $minFile)
4206
			echo '
4207
	<link rel="stylesheet" href="', $minFile['fileUrl'], $minSuccessful && isset($minSeed) ? $minSeed : '', '">';
4208
	}
4209
4210
	// Print the rest after the minified files.
4211
	if (!empty($normal))
4212
		foreach ($normal as $nf)
4213
		{
4214
			echo '
4215
	<link rel="stylesheet" href="', $nf['url'], '"';
4216
4217
			if (!empty($nf['attributes']))
4218
				foreach ($nf['attributes'] as $key => $value)
4219
				{
4220
					if (is_bool($value))
4221
						echo !empty($value) ? ' ' . $key : '';
4222
					else
4223
						echo ' ', $key, '="', $value, '"';
4224
				}
4225
4226
			echo '>';
4227
		}
4228
4229
	if ($db_show_debug === true)
4230
	{
4231
		// Try to keep only what's useful.
4232
		$repl = array($boardurl . '/Themes/' => '', $boardurl . '/' => '');
4233
		foreach ($context['css_files'] as $file)
4234
			$context['debug']['sheets'][] = strtr($file['fileName'], $repl);
4235
	}
4236
4237
	if (!empty($context['css_header']))
4238
	{
4239
		echo '
4240
	<style>';
4241
4242
		foreach ($context['css_header'] as $css)
4243
			echo $css . '
4244
	';
4245
4246
		echo '
4247
	</style>';
4248
	}
4249
}
4250
4251
/**
4252
 * Get an array of previously defined files and adds them to our main minified files.
4253
 * Sets a one day cache to avoid re-creating a file on every request.
4254
 *
4255
 * @param array $data The files to minify.
4256
 * @param string $type either css or js.
4257
 * @return array Info about the minified file, or about the original files if the minify process failed.
4258
 */
4259
function custMinify($data, $type)
4260
{
4261
	global $settings, $txt;
4262
4263
	$types = array('css', 'js');
4264
	$type = !empty($type) && in_array($type, $types) ? $type : false;
4265
	$data = is_array($data) ? $data : array();
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
4266
4267
	if (empty($type) || empty($data))
4268
		return $data;
4269
4270
	// Different pages include different files, so we use a hash to label the different combinations
4271
	$hash = md5(implode(' ', array_map(function($file)
4272
	{
4273
		return $file['filePath'] . '-' . $file['mtime'];
4274
	}, $data)));
4275
4276
	// Is this a deferred or asynchronous JavaScript file?
4277
	$async = $type === 'js';
4278
	$defer = $type === 'js';
4279
	if ($type === 'js')
4280
	{
4281
		foreach ($data as $id => $file)
4282
		{
4283
			// A minified script should only be loaded asynchronously if all its components wanted to be.
4284
			if (empty($file['options']['async']))
4285
				$async = false;
4286
4287
			// A minified script should only be deferred if all its components wanted to be.
4288
			if (empty($file['options']['defer']))
4289
				$defer = false;
4290
		}
4291
	}
4292
4293
	// Did we already do this?
4294
	$minified_file = $settings['theme_dir'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/minified_' . $hash . '.' . $type;
4295
	$already_exists = file_exists($minified_file);
4296
4297
	// Already done?
4298
	if ($already_exists)
4299
	{
4300
		return array('smf_minified' => array(
4301
			'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file),
4302
			'filePath' => $minified_file,
4303
			'fileName' => basename($minified_file),
4304
			'options' => array('async' => !empty($async), 'defer' => !empty($defer)),
4305
		));
4306
	}
4307
	// File has to exist. If it doesn't, try to create it.
4308
	elseif (@fopen($minified_file, 'w') === false || !smf_chmod($minified_file))
4309
	{
4310
		loadLanguage('Errors');
4311
		log_error(sprintf($txt['file_not_created'], $minified_file), 'general');
4312
4313
		// The process failed, so roll back to print each individual file.
4314
		return $data;
4315
	}
4316
4317
	// No namespaces, sorry!
4318
	$classType = 'MatthiasMullie\\Minify\\' . strtoupper($type);
4319
4320
	$minifier = new $classType();
4321
4322
	foreach ($data as $id => $file)
4323
	{
4324
		$toAdd = !empty($file['filePath']) && file_exists($file['filePath']) ? $file['filePath'] : false;
4325
4326
		// The file couldn't be located so it won't be added. Log this error.
4327
		if (empty($toAdd))
4328
		{
4329
			loadLanguage('Errors');
4330
			log_error(sprintf($txt['file_minimize_fail'], !empty($file['fileName']) ? $file['fileName'] : $id), 'general');
4331
			continue;
4332
		}
4333
4334
		// Add this file to the list.
4335
		$minifier->add($toAdd);
4336
	}
4337
4338
	// Create the file.
4339
	$minifier->minify($minified_file);
4340
	unset($minifier);
4341
	clearstatcache();
4342
4343
	// Minify process failed.
4344
	if (!filesize($minified_file))
4345
	{
4346
		loadLanguage('Errors');
4347
		log_error(sprintf($txt['file_not_created'], $minified_file), 'general');
4348
4349
		// The process failed so roll back to print each individual file.
4350
		return $data;
4351
	}
4352
4353
	return array('smf_minified' => array(
4354
		'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file),
4355
		'filePath' => $minified_file,
4356
		'fileName' => basename($minified_file),
4357
		'options' => array('async' => $async, 'defer' => $defer),
4358
	));
4359
}
4360
4361
/**
4362
 * Clears out old minimized CSS and JavaScript files and ensures $modSettings['browser_cache'] is up to date
4363
 */
4364
function deleteAllMinified()
4365
{
4366
	global $smcFunc, $txt, $modSettings;
4367
4368
	$not_deleted = array();
4369
	$most_recent = 0;
4370
4371
	// Kinda sucks that we need to do another query to get all the theme dirs, but c'est la vie.
4372
	$request = $smcFunc['db_query']('', '
4373
		SELECT id_theme AS id, value AS dir
4374
		FROM {db_prefix}themes
4375
		WHERE variable = {string:var}',
4376
		array(
4377
			'var' => 'theme_dir',
4378
		)
4379
	);
4380
	while ($theme = $smcFunc['db_fetch_assoc']($request))
4381
	{
4382
		foreach (array('css', 'js') as $type)
4383
		{
4384
			foreach (glob(rtrim($theme['dir'], '/') . '/' . ($type == 'css' ? 'css' : 'scripts') . '/*.' . $type) as $filename)
4385
			{
4386
				// We want to find the most recent mtime of non-minified files
4387
				if (strpos(pathinfo($filename, PATHINFO_BASENAME), 'minified') === false)
4388
					$most_recent = max($modSettings['browser_cache'], (int) @filemtime($filename));
4389
4390
				// Try to delete minified files. Add them to our error list if that fails.
4391
				elseif (!@unlink($filename))
4392
					$not_deleted[] = $filename;
4393
			}
4394
		}
4395
	}
4396
	$smcFunc['db_free_result']($request);
4397
4398
	// This setting tracks the most recent modification time of any of our CSS and JS files
4399
	if ($most_recent > $modSettings['browser_cache'])
4400
		updateSettings(array('browser_cache' => $most_recent));
4401
4402
	// If any of the files could not be deleted, log an error about it.
4403
	if (!empty($not_deleted))
4404
	{
4405
		loadLanguage('Errors');
4406
		log_error(sprintf($txt['unlink_minimized_fail'], implode('<br>', $not_deleted)), 'general');
4407
	}
4408
}
4409
4410
/**
4411
 * Get an attachment's encrypted filename. If $new is true, won't check for file existence.
4412
 *
4413
 * @todo this currently returns the hash if new, and the full filename otherwise.
4414
 * Something messy like that.
4415
 * @todo and of course everything relies on this behavior and work around it. :P.
4416
 * Converters included.
4417
 *
4418
 * @param string $filename The name of the file
4419
 * @param int $attachment_id The ID of the attachment
4420
 * @param string|null $dir Which directory it should be in (null to use current one)
4421
 * @param bool $new Whether this is a new attachment
4422
 * @param string $file_hash The file hash
4423
 * @return string The path to the file
4424
 */
4425
function getAttachmentFilename($filename, $attachment_id, $dir = null, $new = false, $file_hash = '')
4426
{
4427
	global $modSettings, $smcFunc;
4428
4429
	// Just make up a nice hash...
4430
	if ($new)
4431
		return sha1(md5($filename . time()) . mt_rand());
4432
4433
	// Just make sure that attachment id is only a int
4434
	$attachment_id = (int) $attachment_id;
4435
4436
	// Grab the file hash if it wasn't added.
4437
	// Left this for legacy.
4438
	if ($file_hash === '')
4439
	{
4440
		$request = $smcFunc['db_query']('', '
4441
			SELECT file_hash
4442
			FROM {db_prefix}attachments
4443
			WHERE id_attach = {int:id_attach}',
4444
			array(
4445
				'id_attach' => $attachment_id,
4446
			)
4447
		);
4448
4449
		if ($smcFunc['db_num_rows']($request) === 0)
4450
			return false;
4451
4452
		list ($file_hash) = $smcFunc['db_fetch_row']($request);
4453
		$smcFunc['db_free_result']($request);
4454
	}
4455
4456
	// Still no hash? mmm...
4457
	if (empty($file_hash))
4458
		$file_hash = sha1(md5($filename . time()) . mt_rand());
4459
4460
	// Are we using multiple directories?
4461
	if (is_array($modSettings['attachmentUploadDir']))
4462
		$path = $modSettings['attachmentUploadDir'][$dir];
4463
4464
	else
4465
		$path = $modSettings['attachmentUploadDir'];
4466
4467
	return $path . '/' . $attachment_id . '_' . $file_hash . '.dat';
4468
}
4469
4470
/**
4471
 * Convert a single IP to a ranged IP.
4472
 * internal function used to convert a user-readable format to a format suitable for the database.
4473
 *
4474
 * @param string $fullip The full IP
4475
 * @return array An array of IP parts
4476
 */
4477
function ip2range($fullip)
4478
{
4479
	// Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.)
4480
	if ($fullip == 'unknown')
4481
		$fullip = '255.255.255.255';
4482
4483
	$ip_parts = explode('-', $fullip);
4484
	$ip_array = array();
4485
4486
	// if ip 22.12.31.21
4487
	if (count($ip_parts) == 1 && isValidIP($fullip))
4488
	{
4489
		$ip_array['low'] = $fullip;
4490
		$ip_array['high'] = $fullip;
4491
		return $ip_array;
4492
	} // if ip 22.12.* -> 22.12.* - 22.12.*
4493
	elseif (count($ip_parts) == 1)
4494
	{
4495
		$ip_parts[0] = $fullip;
4496
		$ip_parts[1] = $fullip;
4497
	}
4498
4499
	// if ip 22.12.31.21-12.21.31.21
4500
	if (count($ip_parts) == 2 && isValidIP($ip_parts[0]) && isValidIP($ip_parts[1]))
4501
	{
4502
		$ip_array['low'] = $ip_parts[0];
4503
		$ip_array['high'] = $ip_parts[1];
4504
		return $ip_array;
4505
	}
4506
	elseif (count($ip_parts) == 2) // if ip 22.22.*-22.22.*
4507
	{
4508
		$valid_low = isValidIP($ip_parts[0]);
4509
		$valid_high = isValidIP($ip_parts[1]);
4510
		$count = 0;
4511
		$mode = (preg_match('/:/', $ip_parts[0]) > 0 ? ':' : '.');
4512
		$max = ($mode == ':' ? 'ffff' : '255');
4513
		$min = 0;
4514
		if (!$valid_low)
4515
		{
4516
			$ip_parts[0] = preg_replace('/\*/', '0', $ip_parts[0]);
4517
			$valid_low = isValidIP($ip_parts[0]);
4518
			while (!$valid_low)
4519
			{
4520
				$ip_parts[0] .= $mode . $min;
4521
				$valid_low = isValidIP($ip_parts[0]);
4522
				$count++;
4523
				if ($count > 9) break;
4524
			}
4525
		}
4526
4527
		$count = 0;
4528
		if (!$valid_high)
4529
		{
4530
			$ip_parts[1] = preg_replace('/\*/', $max, $ip_parts[1]);
4531
			$valid_high = isValidIP($ip_parts[1]);
4532
			while (!$valid_high)
4533
			{
4534
				$ip_parts[1] .= $mode . $max;
4535
				$valid_high = isValidIP($ip_parts[1]);
4536
				$count++;
4537
				if ($count > 9) break;
4538
			}
4539
		}
4540
4541
		if ($valid_high && $valid_low)
4542
		{
4543
			$ip_array['low'] = $ip_parts[0];
4544
			$ip_array['high'] = $ip_parts[1];
4545
		}
4546
	}
4547
4548
	return $ip_array;
4549
}
4550
4551
/**
4552
 * Lookup an IP; try shell_exec first because we can do a timeout on it.
4553
 *
4554
 * @param string $ip The IP to get the hostname from
4555
 * @return string The hostname
4556
 */
4557
function host_from_ip($ip)
4558
{
4559
	global $modSettings;
4560
4561
	if (($host = cache_get_data('hostlookup-' . $ip, 600)) !== null)
4562
		return $host;
4563
	$t = microtime(true);
4564
4565
	// Try the Linux host command, perhaps?
4566
	if (!isset($host) && (strpos(strtolower(PHP_OS), 'win') === false || strpos(strtolower(PHP_OS), 'darwin') !== false) && mt_rand(0, 1) == 1)
4567
	{
4568
		if (!isset($modSettings['host_to_dis']))
4569
			$test = @shell_exec('host -W 1 ' . @escapeshellarg($ip));
4570
		else
4571
			$test = @shell_exec('host ' . @escapeshellarg($ip));
4572
4573
		// Did host say it didn't find anything?
4574
		if (strpos($test, 'not found') !== false)
4575
			$host = '';
4576
		// Invalid server option?
4577
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
4578
			updateSettings(array('host_to_dis' => 1));
4579
		// Maybe it found something, after all?
4580
		elseif (preg_match('~\s([^\s]+?)\.\s~', $test, $match) == 1)
4581
			$host = $match[1];
4582
	}
4583
4584
	// This is nslookup; usually only Windows, but possibly some Unix?
4585
	if (!isset($host) && stripos(PHP_OS, 'win') !== false && strpos(strtolower(PHP_OS), 'darwin') === false && mt_rand(0, 1) == 1)
4586
	{
4587
		$test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
4588
		if (strpos($test, 'Non-existent domain') !== false)
4589
			$host = '';
4590
		elseif (preg_match('~Name:\s+([^\s]+)~', $test, $match) == 1)
4591
			$host = $match[1];
4592
	}
4593
4594
	// This is the last try :/.
4595
	if (!isset($host) || $host === false)
4596
		$host = @gethostbyaddr($ip);
4597
4598
	// It took a long time, so let's cache it!
4599
	if (microtime(true) - $t > 0.5)
4600
		cache_put_data('hostlookup-' . $ip, $host, 600);
4601
4602
	return $host;
4603
}
4604
4605
/**
4606
 * Chops a string into words and prepares them to be inserted into (or searched from) the database.
4607
 *
4608
 * @param string $text The text to split into words
4609
 * @param int $max_chars The maximum number of characters per word
4610
 * @param bool $encrypt Whether to encrypt the results
4611
 * @return array An array of ints or words depending on $encrypt
4612
 */
4613
function text2words($text, $max_chars = 20, $encrypt = false)
4614
{
4615
	global $smcFunc, $context;
4616
4617
	// Upgrader may be working on old DBs...
4618
	if (!isset($context['utf8']))
4619
		$context['utf8'] = false;
4620
4621
	// Step 1: Remove entities/things we don't consider words:
4622
	$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>' => ' ')));
4623
4624
	// Step 2: Entities we left to letters, where applicable, lowercase.
4625
	$words = un_htmlspecialchars($smcFunc['strtolower']($words));
4626
4627
	// Step 3: Ready to split apart and index!
4628
	$words = explode(' ', $words);
4629
4630
	if ($encrypt)
4631
	{
4632
		$possible_chars = array_flip(array_merge(range(46, 57), range(65, 90), range(97, 122)));
4633
		$returned_ints = array();
4634
		foreach ($words as $word)
4635
		{
4636
			if (($word = trim($word, '-_\'')) !== '')
4637
			{
4638
				$encrypted = substr(crypt($word, 'uk'), 2, $max_chars);
4639
				$total = 0;
4640
				for ($i = 0; $i < $max_chars; $i++)
4641
					$total += $possible_chars[ord($encrypted[$i])] * pow(63, $i);
4642
				$returned_ints[] = $max_chars == 4 ? min($total, 16777215) : $total;
4643
			}
4644
		}
4645
		return array_unique($returned_ints);
4646
	}
4647
	else
4648
	{
4649
		// Trim characters before and after and add slashes for database insertion.
4650
		$returned_words = array();
4651
		foreach ($words as $word)
4652
			if (($word = trim($word, '-_\'')) !== '')
4653
				$returned_words[] = $max_chars === null ? $word : substr($word, 0, $max_chars);
4654
4655
		// Filter out all words that occur more than once.
4656
		return array_unique($returned_words);
4657
	}
4658
}
4659
4660
/**
4661
 * Creates an image/text button
4662
 *
4663
 * @deprecated since 2.1
4664
 * @param string $name The name of the button (should be a main_icons class or the name of an image)
4665
 * @param string $alt The alt text
4666
 * @param string $label The $txt string to use as the label
4667
 * @param string $custom Custom text/html to add to the img tag (only when using an actual image)
4668
 * @param boolean $force_use Whether to force use of this when template_create_button is available
4669
 * @return string The HTML to display the button
4670
 */
4671
function create_button($name, $alt, $label = '', $custom = '', $force_use = false)
4672
{
4673
	global $settings, $txt;
4674
4675
	// Does the current loaded theme have this and we are not forcing the usage of this function?
4676
	if (function_exists('template_create_button') && !$force_use)
4677
		return template_create_button($name, $alt, $label = '', $custom = '');
4678
4679
	if (!$settings['use_image_buttons'])
4680
		return $txt[$alt];
4681
	elseif (!empty($settings['use_buttons']))
4682
		return '<span class="main_icons ' . $name . '" alt="' . $txt[$alt] . '"></span>' . ($label != '' ? '&nbsp;<strong>' . $txt[$label] . '</strong>' : '');
4683
	else
4684
		return '<img src="' . $settings['lang_images_url'] . '/' . $name . '" alt="' . $txt[$alt] . '" ' . $custom . '>';
4685
}
4686
4687
/**
4688
 * Sets up all of the top menu buttons
4689
 * Saves them in the cache if it is available and on
4690
 * Places the results in $context
4691
 */
4692
function setupMenuContext()
4693
{
4694
	global $context, $modSettings, $user_info, $txt, $scripturl, $sourcedir, $settings, $smcFunc, $cache_enable;
4695
4696
	// Set up the menu privileges.
4697
	$context['allow_search'] = !empty($modSettings['allow_guestAccess']) ? allowedTo('search_posts') : (!$user_info['is_guest'] && allowedTo('search_posts'));
4698
	$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'));
4699
4700
	$context['allow_memberlist'] = allowedTo('view_mlist');
4701
	$context['allow_calendar'] = allowedTo('calendar_view') && !empty($modSettings['cal_enabled']);
4702
	$context['allow_moderation_center'] = $context['user']['can_mod'];
4703
	$context['allow_pm'] = allowedTo('pm_read');
4704
4705
	$cacheTime = $modSettings['lastActive'] * 60;
4706
4707
	// Initial "can you post an event in the calendar" option - but this might have been set in the calendar already.
4708
	if (!isset($context['allow_calendar_event']))
4709
	{
4710
		$context['allow_calendar_event'] = $context['allow_calendar'] && allowedTo('calendar_post');
4711
4712
		// If you don't allow events not linked to posts and you're not an admin, we have more work to do...
4713
		if ($context['allow_calendar'] && $context['allow_calendar_event'] && empty($modSettings['cal_allow_unlinked']) && !$user_info['is_admin'])
4714
		{
4715
			$boards_can_post = boardsAllowedTo('post_new');
4716
			$context['allow_calendar_event'] &= !empty($boards_can_post);
4717
		}
4718
	}
4719
4720
	// There is some menu stuff we need to do if we're coming at this from a non-guest perspective.
4721
	if (!$context['user']['is_guest'])
4722
	{
4723
		addInlineJavaScript('
4724
	var user_menus = new smc_PopupMenu();
4725
	user_menus.add("profile", "' . $scripturl . '?action=profile;area=popup");
4726
	user_menus.add("alerts", "' . $scripturl . '?action=profile;area=alerts_popup;u=' . $context['user']['id'] . '");', true);
4727
		if ($context['allow_pm'])
4728
			addInlineJavaScript('
4729
	user_menus.add("pm", "' . $scripturl . '?action=pm;sa=popup");', true);
4730
4731
		if (!empty($modSettings['enable_ajax_alerts']))
4732
		{
4733
			require_once($sourcedir . '/Subs-Notify.php');
4734
4735
			$timeout = getNotifyPrefs($context['user']['id'], 'alert_timeout', true);
4736
			$timeout = empty($timeout) ? 10000 : $timeout[$context['user']['id']]['alert_timeout'] * 1000;
4737
4738
			addInlineJavaScript('
4739
	var new_alert_title = "' . $context['forum_name'] . '";
4740
	var alert_timeout = ' . $timeout . ';');
4741
			loadJavaScriptFile('alerts.js', array('minimize' => true), 'smf_alerts');
4742
		}
4743
	}
4744
4745
	// All the buttons we can possible want and then some, try pulling the final list of buttons from cache first.
4746
	if (($menu_buttons = cache_get_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $cacheTime)) === null || time() - $cacheTime <= $modSettings['settings_updated'])
4747
	{
4748
		$buttons = array(
4749
			'home' => array(
4750
				'title' => $txt['home'],
4751
				'href' => $scripturl,
4752
				'show' => true,
4753
				'sub_buttons' => array(
4754
				),
4755
				'is_last' => $context['right_to_left'],
4756
			),
4757
			'search' => array(
4758
				'title' => $txt['search'],
4759
				'href' => $scripturl . '?action=search',
4760
				'show' => $context['allow_search'],
4761
				'sub_buttons' => array(
4762
				),
4763
			),
4764
			'admin' => array(
4765
				'title' => $txt['admin'],
4766
				'href' => $scripturl . '?action=admin',
4767
				'show' => $context['allow_admin'],
4768
				'sub_buttons' => array(
4769
					'featuresettings' => array(
4770
						'title' => $txt['modSettings_title'],
4771
						'href' => $scripturl . '?action=admin;area=featuresettings',
4772
						'show' => allowedTo('admin_forum'),
4773
					),
4774
					'packages' => array(
4775
						'title' => $txt['package'],
4776
						'href' => $scripturl . '?action=admin;area=packages',
4777
						'show' => allowedTo('admin_forum'),
4778
					),
4779
					'errorlog' => array(
4780
						'title' => $txt['errorlog'],
4781
						'href' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc',
4782
						'show' => allowedTo('admin_forum') && !empty($modSettings['enableErrorLogging']),
4783
					),
4784
					'permissions' => array(
4785
						'title' => $txt['edit_permissions'],
4786
						'href' => $scripturl . '?action=admin;area=permissions',
4787
						'show' => allowedTo('manage_permissions'),
4788
					),
4789
					'memberapprove' => array(
4790
						'title' => $txt['approve_members_waiting'],
4791
						'href' => $scripturl . '?action=admin;area=viewmembers;sa=browse;type=approve',
4792
						'show' => !empty($context['unapproved_members']),
4793
						'is_last' => true,
4794
					),
4795
				),
4796
			),
4797
			'moderate' => array(
4798
				'title' => $txt['moderate'],
4799
				'href' => $scripturl . '?action=moderate',
4800
				'show' => $context['allow_moderation_center'],
4801
				'sub_buttons' => array(
4802
					'modlog' => array(
4803
						'title' => $txt['modlog_view'],
4804
						'href' => $scripturl . '?action=moderate;area=modlog',
4805
						'show' => !empty($modSettings['modlog_enabled']) && !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
4806
					),
4807
					'poststopics' => array(
4808
						'title' => $txt['mc_unapproved_poststopics'],
4809
						'href' => $scripturl . '?action=moderate;area=postmod;sa=posts',
4810
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
4811
					),
4812
					'attachments' => array(
4813
						'title' => $txt['mc_unapproved_attachments'],
4814
						'href' => $scripturl . '?action=moderate;area=attachmod;sa=attachments',
4815
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
4816
					),
4817
					'reports' => array(
4818
						'title' => $txt['mc_reported_posts'],
4819
						'href' => $scripturl . '?action=moderate;area=reportedposts',
4820
						'show' => !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
4821
					),
4822
					'reported_members' => array(
4823
						'title' => $txt['mc_reported_members'],
4824
						'href' => $scripturl . '?action=moderate;area=reportedmembers',
4825
						'show' => allowedTo('moderate_forum'),
4826
						'is_last' => true,
4827
					)
4828
				),
4829
			),
4830
			'calendar' => array(
4831
				'title' => $txt['calendar'],
4832
				'href' => $scripturl . '?action=calendar',
4833
				'show' => $context['allow_calendar'],
4834
				'sub_buttons' => array(
4835
					'view' => array(
4836
						'title' => $txt['calendar_menu'],
4837
						'href' => $scripturl . '?action=calendar',
4838
						'show' => $context['allow_calendar_event'],
4839
					),
4840
					'post' => array(
4841
						'title' => $txt['calendar_post_event'],
4842
						'href' => $scripturl . '?action=calendar;sa=post',
4843
						'show' => $context['allow_calendar_event'],
4844
						'is_last' => true,
4845
					),
4846
				),
4847
			),
4848
			'mlist' => array(
4849
				'title' => $txt['members_title'],
4850
				'href' => $scripturl . '?action=mlist',
4851
				'show' => $context['allow_memberlist'],
4852
				'sub_buttons' => array(
4853
					'mlist_view' => array(
4854
						'title' => $txt['mlist_menu_view'],
4855
						'href' => $scripturl . '?action=mlist',
4856
						'show' => true,
4857
					),
4858
					'mlist_search' => array(
4859
						'title' => $txt['mlist_search'],
4860
						'href' => $scripturl . '?action=mlist;sa=search',
4861
						'show' => true,
4862
						'is_last' => true,
4863
					),
4864
				),
4865
				'is_last' => !$context['right_to_left'] && (!$user_info['is_guest'] || !$context['can_register']),
4866
			),
4867
			'signup' => array(
4868
				'title' => $txt['register'],
4869
				'href' => $scripturl . '?action=signup',
4870
				'show' => $user_info['is_guest'] && $context['can_register'],
4871
				'sub_buttons' => array(
4872
				),
4873
				'is_last' => !$context['right_to_left'],
4874
			),
4875
		);
4876
4877
		// Allow editing menu buttons easily.
4878
		call_integration_hook('integrate_menu_buttons', array(&$buttons));
4879
4880
		// Now we put the buttons in the context so the theme can use them.
4881
		$menu_buttons = array();
4882
		foreach ($buttons as $act => $button)
4883
			if (!empty($button['show']))
4884
			{
4885
				$button['active_button'] = false;
4886
4887
				// This button needs some action.
4888
				if (isset($button['action_hook']))
4889
					$needs_action_hook = true;
4890
4891
				// Make sure the last button truly is the last button.
4892
				if (!empty($button['is_last']))
4893
				{
4894
					if (isset($last_button))
4895
						unset($menu_buttons[$last_button]['is_last']);
4896
					$last_button = $act;
4897
				}
4898
4899
				// Go through the sub buttons if there are any.
4900
				if (!empty($button['sub_buttons']))
4901
					foreach ($button['sub_buttons'] as $key => $subbutton)
4902
					{
4903
						if (empty($subbutton['show']))
4904
							unset($button['sub_buttons'][$key]);
4905
4906
						// 2nd level sub buttons next...
4907
						if (!empty($subbutton['sub_buttons']))
4908
						{
4909
							foreach ($subbutton['sub_buttons'] as $key2 => $sub_button2)
4910
							{
4911
								if (empty($sub_button2['show']))
4912
									unset($button['sub_buttons'][$key]['sub_buttons'][$key2]);
4913
							}
4914
						}
4915
					}
4916
4917
				// Does this button have its own icon?
4918
				if (isset($button['icon']) && file_exists($settings['theme_dir'] . '/images/' . $button['icon']))
4919
					$button['icon'] = '<img src="' . $settings['images_url'] . '/' . $button['icon'] . '" alt="">';
4920
				elseif (isset($button['icon']) && file_exists($settings['default_theme_dir'] . '/images/' . $button['icon']))
4921
					$button['icon'] = '<img src="' . $settings['default_images_url'] . '/' . $button['icon'] . '" alt="">';
4922
				elseif (isset($button['icon']))
4923
					$button['icon'] = '<span class="main_icons ' . $button['icon'] . '"></span>';
4924
				else
4925
					$button['icon'] = '<span class="main_icons ' . $act . '"></span>';
4926
4927
				$menu_buttons[$act] = $button;
4928
			}
4929
4930
		if (!empty($cache_enable) && $cache_enable >= 2)
4931
			cache_put_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $menu_buttons, $cacheTime);
4932
	}
4933
4934
	$context['menu_buttons'] = $menu_buttons;
4935
4936
	// Logging out requires the session id in the url.
4937
	if (isset($context['menu_buttons']['logout']))
4938
		$context['menu_buttons']['logout']['href'] = sprintf($context['menu_buttons']['logout']['href'], $context['session_var'], $context['session_id']);
4939
4940
	// Figure out which action we are doing so we can set the active tab.
4941
	// Default to home.
4942
	$current_action = 'home';
4943
4944
	if (isset($context['menu_buttons'][$context['current_action']]))
4945
		$current_action = $context['current_action'];
4946
	elseif ($context['current_action'] == 'search2')
4947
		$current_action = 'search';
4948
	elseif ($context['current_action'] == 'theme')
4949
		$current_action = isset($_REQUEST['sa']) && $_REQUEST['sa'] == 'pick' ? 'profile' : 'admin';
4950
	elseif ($context['current_action'] == 'register2')
4951
		$current_action = 'register';
4952
	elseif ($context['current_action'] == 'login2' || ($user_info['is_guest'] && $context['current_action'] == 'reminder'))
4953
		$current_action = 'login';
4954
	elseif ($context['current_action'] == 'groups' && $context['allow_moderation_center'])
4955
		$current_action = 'moderate';
4956
4957
	// There are certain exceptions to the above where we don't want anything on the menu highlighted.
4958
	if ($context['current_action'] == 'profile' && !empty($context['user']['is_owner']))
4959
	{
4960
		$current_action = !empty($_GET['area']) && $_GET['area'] == 'showalerts' ? 'self_alerts' : 'self_profile';
4961
		$context[$current_action] = true;
4962
	}
4963
	elseif ($context['current_action'] == 'pm')
4964
	{
4965
		$current_action = 'self_pm';
4966
		$context['self_pm'] = true;
4967
	}
4968
4969
	$context['total_mod_reports'] = 0;
4970
	$context['total_admin_reports'] = 0;
4971
4972
	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']))
4973
	{
4974
		$context['total_mod_reports'] = $context['open_mod_reports'];
4975
		$context['menu_buttons']['moderate']['sub_buttons']['reports']['amt'] = $context['open_mod_reports'];
4976
	}
4977
4978
	// Show how many errors there are
4979
	if (!empty($context['menu_buttons']['admin']['sub_buttons']['errorlog']))
4980
	{
4981
		// Get an error count, if necessary
4982
		if (!isset($context['num_errors']))
4983
		{
4984
			$query = $smcFunc['db_query']('', '
4985
				SELECT COUNT(*)
4986
				FROM {db_prefix}log_errors',
4987
				array()
4988
			);
4989
4990
			list($context['num_errors']) = $smcFunc['db_fetch_row']($query);
4991
			$smcFunc['db_free_result']($query);
4992
		}
4993
4994
		if (!empty($context['num_errors']))
4995
		{
4996
			$context['total_admin_reports'] += $context['num_errors'];
4997
			$context['menu_buttons']['admin']['sub_buttons']['errorlog']['amt'] = $context['num_errors'];
4998
		}
4999
	}
5000
5001
	// Show number of reported members
5002
	if (!empty($context['open_member_reports']) && !empty($context['menu_buttons']['moderate']['sub_buttons']['reported_members']))
5003
	{
5004
		$context['total_mod_reports'] += $context['open_member_reports'];
5005
		$context['menu_buttons']['moderate']['sub_buttons']['reported_members']['amt'] = $context['open_member_reports'];
5006
	}
5007
5008
	if (!empty($context['unapproved_members']) && !empty($context['menu_buttons']['admin']))
5009
	{
5010
		$context['menu_buttons']['admin']['sub_buttons']['memberapprove']['amt'] = $context['unapproved_members'];
5011
		$context['total_admin_reports'] += $context['unapproved_members'];
5012
	}
5013
5014
	if ($context['total_admin_reports'] > 0 && !empty($context['menu_buttons']['admin']))
5015
	{
5016
		$context['menu_buttons']['admin']['amt'] = $context['total_admin_reports'];
5017
	}
5018
5019
	// Do we have any open reports?
5020
	if ($context['total_mod_reports'] > 0 && !empty($context['menu_buttons']['moderate']))
5021
	{
5022
		$context['menu_buttons']['moderate']['amt'] = $context['total_mod_reports'];
5023
	}
5024
5025
	// Not all actions are simple.
5026
	if (!empty($needs_action_hook))
5027
		call_integration_hook('integrate_current_action', array(&$current_action));
5028
5029
	if (isset($context['menu_buttons'][$current_action]))
5030
		$context['menu_buttons'][$current_action]['active_button'] = true;
5031
}
5032
5033
/**
5034
 * Generate a random seed and ensure it's stored in settings.
5035
 */
5036
function smf_seed_generator()
5037
{
5038
	updateSettings(array('rand_seed' => microtime(true)));
5039
}
5040
5041
/**
5042
 * Process functions of an integration hook.
5043
 * calls all functions of the given hook.
5044
 * supports static class method calls.
5045
 *
5046
 * @param string $hook The hook name
5047
 * @param array $parameters An array of parameters this hook implements
5048
 * @return array The results of the functions
5049
 */
5050
function call_integration_hook($hook, $parameters = array())
5051
{
5052
	global $modSettings, $settings, $boarddir, $sourcedir, $db_show_debug;
5053
	global $context, $txt;
5054
5055
	if ($db_show_debug === true)
5056
		$context['debug']['hooks'][] = $hook;
5057
5058
	// Need to have some control.
5059
	if (!isset($context['instances']))
5060
		$context['instances'] = array();
5061
5062
	$results = array();
5063
	if (empty($modSettings[$hook]))
5064
		return $results;
5065
5066
	$functions = explode(',', $modSettings[$hook]);
5067
	// Loop through each function.
5068
	foreach ($functions as $function)
5069
	{
5070
		// Hook has been marked as "disabled". Skip it!
5071
		if (strpos($function, '!') !== false)
5072
			continue;
5073
5074
		$call = call_helper($function, true);
5075
5076
		// Is it valid?
5077
		if (!empty($call))
5078
			$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 $function 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

5078
			$results[$function] = call_user_func_array(/** @scrutinizer ignore-type */ $call, $parameters);
Loading history...
5079
		// This failed, but we want to do so silently.
5080
		elseif (!empty($function) && !empty($context['ignore_hook_errors']))
5081
			return $results;
5082
		// Whatever it was suppose to call, it failed :(
5083
		elseif (!empty($function))
5084
		{
5085
			loadLanguage('Errors');
5086
5087
			// Get a full path to show on error.
5088
			if (strpos($function, '|') !== false)
5089
			{
5090
				list ($file, $string) = explode('|', $function);
5091
				$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'])));
5092
				log_error(sprintf($txt['hook_fail_call_to'], $string, $absPath), 'general');
5093
			}
5094
			// "Assume" the file resides on $boarddir somewhere...
5095
			else
5096
				log_error(sprintf($txt['hook_fail_call_to'], $function, $boarddir), 'general');
5097
		}
5098
	}
5099
5100
	return $results;
5101
}
5102
5103
/**
5104
 * Add a function for integration hook.
5105
 * does nothing if the function is already added.
5106
 *
5107
 * @param string $hook The complete hook name.
5108
 * @param string $function The function name. Can be a call to a method via Class::method.
5109
 * @param bool $permanent If true, updates the value in settings table.
5110
 * @param string $file The file. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
5111
 * @param bool $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
5112
 */
5113
function add_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
5114
{
5115
	global $smcFunc, $modSettings;
5116
5117
	// Any objects?
5118
	if ($object)
5119
		$function = $function . '#';
5120
5121
	// Any files  to load?
5122
	if (!empty($file) && is_string($file))
5123
		$function = $file . (!empty($function) ? '|' . $function : '');
5124
5125
	// Get the correct string.
5126
	$integration_call = $function;
5127
5128
	// Is it going to be permanent?
5129
	if ($permanent)
5130
	{
5131
		$request = $smcFunc['db_query']('', '
5132
			SELECT value
5133
			FROM {db_prefix}settings
5134
			WHERE variable = {string:variable}',
5135
			array(
5136
				'variable' => $hook,
5137
			)
5138
		);
5139
		list ($current_functions) = $smcFunc['db_fetch_row']($request);
5140
		$smcFunc['db_free_result']($request);
5141
5142
		if (!empty($current_functions))
5143
		{
5144
			$current_functions = explode(',', $current_functions);
5145
			if (in_array($integration_call, $current_functions))
5146
				return;
5147
5148
			$permanent_functions = array_merge($current_functions, array($integration_call));
5149
		}
5150
		else
5151
			$permanent_functions = array($integration_call);
5152
5153
		updateSettings(array($hook => implode(',', $permanent_functions)));
5154
	}
5155
5156
	// Make current function list usable.
5157
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
5158
5159
	// Do nothing, if it's already there.
5160
	if (in_array($integration_call, $functions))
5161
		return;
5162
5163
	$functions[] = $integration_call;
5164
	$modSettings[$hook] = implode(',', $functions);
5165
}
5166
5167
/**
5168
 * Remove an integration hook function.
5169
 * Removes the given function from the given hook.
5170
 * Does nothing if the function is not available.
5171
 *
5172
 * @param string $hook The complete hook name.
5173
 * @param string $function The function name. Can be a call to a method via Class::method.
5174
 * @param boolean $permanent Irrelevant for the function itself but need to declare it to match
5175
 * @param string $file The filename. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
5176
 * @param boolean $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
5177
 * @see add_integration_function
5178
 */
5179
function remove_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
5180
{
5181
	global $smcFunc, $modSettings;
5182
5183
	// Any objects?
5184
	if ($object)
5185
		$function = $function . '#';
5186
5187
	// Any files  to load?
5188
	if (!empty($file) && is_string($file))
5189
		$function = $file . '|' . $function;
5190
5191
	// Get the correct string.
5192
	$integration_call = $function;
5193
5194
	// Get the permanent functions.
5195
	$request = $smcFunc['db_query']('', '
5196
		SELECT value
5197
		FROM {db_prefix}settings
5198
		WHERE variable = {string:variable}',
5199
		array(
5200
			'variable' => $hook,
5201
		)
5202
	);
5203
	list ($current_functions) = $smcFunc['db_fetch_row']($request);
5204
	$smcFunc['db_free_result']($request);
5205
5206
	if (!empty($current_functions))
5207
	{
5208
		$current_functions = explode(',', $current_functions);
5209
5210
		if (in_array($integration_call, $current_functions))
5211
			updateSettings(array($hook => implode(',', array_diff($current_functions, array($integration_call)))));
5212
	}
5213
5214
	// Turn the function list into something usable.
5215
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
5216
5217
	// You can only remove it if it's available.
5218
	if (!in_array($integration_call, $functions))
5219
		return;
5220
5221
	$functions = array_diff($functions, array($integration_call));
5222
	$modSettings[$hook] = implode(',', $functions);
5223
}
5224
5225
/**
5226
 * Receives a string and tries to figure it out if its a method or a function.
5227
 * If a method is found, it looks for a "#" which indicates SMF should create a new instance of the given class.
5228
 * Checks the string/array for is_callable() and return false/fatal_lang_error is the given value results in a non callable string/array.
5229
 * Prepare and returns a callable depending on the type of method/function found.
5230
 *
5231
 * @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)
5232
 * @param boolean $return If true, the function will not call the function/method but instead will return the formatted string.
5233
 * @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.
5234
 */
5235
function call_helper($string, $return = false)
5236
{
5237
	global $context, $smcFunc, $txt, $db_show_debug;
5238
5239
	// Really?
5240
	if (empty($string))
5241
		return false;
5242
5243
	// An array? should be a "callable" array IE array(object/class, valid_callable).
5244
	// A closure? should be a callable one.
5245
	if (is_array($string) || $string instanceof Closure)
5246
		return $return ? $string : (is_callable($string) ? call_user_func($string) : false);
5247
5248
	// No full objects, sorry! pass a method or a property instead!
5249
	if (is_object($string))
5250
		return false;
5251
5252
	// Stay vitaminized my friends...
5253
	$string = $smcFunc['htmlspecialchars']($smcFunc['htmltrim']($string));
5254
5255
	// Is there a file to load?
5256
	$string = load_file($string);
5257
5258
	// Loaded file failed
5259
	if (empty($string))
5260
		return false;
5261
5262
	// Found a method.
5263
	if (strpos($string, '::') !== false)
5264
	{
5265
		list ($class, $method) = explode('::', $string);
5266
5267
		// Check if a new object will be created.
5268
		if (strpos($method, '#') !== false)
5269
		{
5270
			// Need to remove the # thing.
5271
			$method = str_replace('#', '', $method);
5272
5273
			// Don't need to create a new instance for every method.
5274
			if (empty($context['instances'][$class]) || !($context['instances'][$class] instanceof $class))
5275
			{
5276
				$context['instances'][$class] = new $class;
5277
5278
				// Add another one to the list.
5279
				if ($db_show_debug === true)
5280
				{
5281
					if (!isset($context['debug']['instances']))
5282
						$context['debug']['instances'] = array();
5283
5284
					$context['debug']['instances'][$class] = $class;
5285
				}
5286
			}
5287
5288
			$func = array($context['instances'][$class], $method);
5289
		}
5290
5291
		// Right then. This is a call to a static method.
5292
		else
5293
			$func = array($class, $method);
5294
	}
5295
5296
	// Nope! just a plain regular function.
5297
	else
5298
		$func = $string;
5299
5300
	// We can't call this helper, but we want to silently ignore this.
5301
	if (!is_callable($func, false, $callable_name) && !empty($context['ignore_hook_errors']))
5302
		return false;
5303
5304
	// Right, we got what we need, time to do some checks.
5305
	elseif (!is_callable($func, false, $callable_name))
5306
	{
5307
		loadLanguage('Errors');
5308
		log_error(sprintf($txt['sub_action_fail'], $callable_name), 'general');
5309
5310
		// Gotta tell everybody.
5311
		return false;
5312
	}
5313
5314
	// Everything went better than expected.
5315
	else
5316
	{
5317
		// What are we gonna do about it?
5318
		if ($return)
5319
			return $func;
5320
5321
		// If this is a plain function, avoid the heat of calling call_user_func().
5322
		else
5323
		{
5324
			if (is_array($func))
5325
				call_user_func($func);
5326
5327
			else
5328
				$func();
5329
		}
5330
	}
5331
}
5332
5333
/**
5334
 * Receives a string and tries to figure it out if it contains info to load a file.
5335
 * Checks for a | (pipe) symbol and tries to load a file with the info given.
5336
 * 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.
5337
 *
5338
 * @param string $string The string containing a valid format.
5339
 * @return string|boolean The given string with the pipe and file info removed. Boolean false if the file couldn't be loaded.
5340
 */
5341
function load_file($string)
5342
{
5343
	global $sourcedir, $txt, $boarddir, $settings, $context;
5344
5345
	if (empty($string))
5346
		return false;
5347
5348
	if (strpos($string, '|') !== false)
5349
	{
5350
		list ($file, $string) = explode('|', $string);
5351
5352
		// Match the wildcards to their regular vars.
5353
		if (empty($settings['theme_dir']))
5354
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
5355
5356
		else
5357
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
5358
5359
		// Load the file if it can be loaded.
5360
		if (file_exists($absPath))
5361
			require_once($absPath);
5362
5363
		// No? try a fallback to $sourcedir
5364
		else
5365
		{
5366
			$absPath = $sourcedir . '/' . $file;
5367
5368
			if (file_exists($absPath))
5369
				require_once($absPath);
5370
5371
			// Sorry, can't do much for you at this point.
5372
			elseif (empty($context['uninstalling']))
5373
			{
5374
				loadLanguage('Errors');
5375
				log_error(sprintf($txt['hook_fail_loading_file'], $absPath), 'general');
5376
5377
				// File couldn't be loaded.
5378
				return false;
5379
			}
5380
		}
5381
	}
5382
5383
	return $string;
5384
}
5385
5386
/**
5387
 * Get the contents of a URL, irrespective of allow_url_fopen.
5388
 *
5389
 * - reads the contents of an http or ftp address and returns the page in a string
5390
 * - will accept up to 3 page redirections (redirectio_level in the function call is private)
5391
 * - if post_data is supplied, the value and length is posted to the given url as form data
5392
 * - URL must be supplied in lowercase
5393
 *
5394
 * @param string $url The URL
5395
 * @param string $post_data The data to post to the given URL
5396
 * @param bool $keep_alive Whether to send keepalive info
5397
 * @param int $redirection_level How many levels of redirection
5398
 * @return string|false The fetched data or false on failure
5399
 */
5400
function fetch_web_data($url, $post_data = '', $keep_alive = false, $redirection_level = 0)
5401
{
5402
	global $webmaster_email, $sourcedir;
5403
	static $keep_alive_dom = null, $keep_alive_fp = null;
5404
5405
	preg_match('~^(http|ftp)(s)?://([^/:]+)(:(\d+))?(.+)$~', $url, $match);
5406
5407
	// No scheme? No data for you!
5408
	if (empty($match[1]))
5409
		return false;
5410
5411
	// An FTP url. We should try connecting and RETRieving it...
5412
	elseif ($match[1] == 'ftp')
5413
	{
5414
		// Include the file containing the ftp_connection class.
5415
		require_once($sourcedir . '/Class-Package.php');
5416
5417
		// Establish a connection and attempt to enable passive mode.
5418
		$ftp = new ftp_connection(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? 21 : $match[5], 'anonymous', $webmaster_email);
5419
		if ($ftp->error !== false || !$ftp->passive())
0 ignored issues
show
introduced by
The condition $ftp->error !== false is always true.
Loading history...
5420
			return false;
5421
5422
		// I want that one *points*!
5423
		fwrite($ftp->connection, 'RETR ' . $match[6] . "\r\n");
5424
5425
		// Since passive mode worked (or we would have returned already!) open the connection.
5426
		$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...
5427
		if (!$fp)
5428
			return false;
5429
5430
		// The server should now say something in acknowledgement.
5431
		$ftp->check_response(150);
5432
5433
		$data = '';
5434
		while (!feof($fp))
5435
			$data .= fread($fp, 4096);
5436
		fclose($fp);
5437
5438
		// All done, right?  Good.
5439
		$ftp->check_response(226);
5440
		$ftp->close();
5441
	}
5442
5443
	// This is more likely; a standard HTTP URL.
5444
	elseif (isset($match[1]) && $match[1] == 'http')
5445
	{
5446
		// First try to use fsockopen, because it is fastest.
5447
		if ($keep_alive && $match[3] == $keep_alive_dom)
5448
			$fp = $keep_alive_fp;
5449
		if (empty($fp))
5450
		{
5451
			// Open the socket on the port we want...
5452
			$fp = @fsockopen(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? ($match[2] ? 443 : 80) : $match[5], $err, $err, 5);
5453
		}
5454
		if (!empty($fp))
5455
		{
5456
			if ($keep_alive)
5457
			{
5458
				$keep_alive_dom = $match[3];
5459
				$keep_alive_fp = $fp;
5460
			}
5461
5462
			// I want this, from there, and I'm not going to be bothering you for more (probably.)
5463
			if (empty($post_data))
5464
			{
5465
				fwrite($fp, 'GET ' . ($match[6] !== '/' ? str_replace(' ', '%20', $match[6]) : '') . ' HTTP/1.0' . "\r\n");
5466
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
5467
				fwrite($fp, 'user-agent: '. SMF_USER_AGENT . "\r\n");
5468
				if ($keep_alive)
5469
					fwrite($fp, 'connection: Keep-Alive' . "\r\n\r\n");
5470
				else
5471
					fwrite($fp, 'connection: close' . "\r\n\r\n");
5472
			}
5473
			else
5474
			{
5475
				fwrite($fp, 'POST ' . ($match[6] !== '/' ? $match[6] : '') . ' HTTP/1.0' . "\r\n");
5476
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
5477
				fwrite($fp, 'user-agent: '. SMF_USER_AGENT . "\r\n");
5478
				if ($keep_alive)
5479
					fwrite($fp, 'connection: Keep-Alive' . "\r\n");
5480
				else
5481
					fwrite($fp, 'connection: close' . "\r\n");
5482
				fwrite($fp, 'content-type: application/x-www-form-urlencoded' . "\r\n");
5483
				fwrite($fp, 'content-length: ' . strlen($post_data) . "\r\n\r\n");
5484
				fwrite($fp, $post_data);
5485
			}
5486
5487
			$response = fgets($fp, 768);
5488
5489
			// Redirect in case this location is permanently or temporarily moved.
5490
			if ($redirection_level < 3 && preg_match('~^HTTP/\S+\s+30[127]~i', $response) === 1)
5491
			{
5492
				$header = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $header is dead and can be removed.
Loading history...
5493
				$location = '';
5494
				while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
5495
					if (stripos($header, 'location:') !== false)
5496
						$location = trim(substr($header, strpos($header, ':') + 1));
5497
5498
				if (empty($location))
5499
					return false;
5500
				else
5501
				{
5502
					if (!$keep_alive)
5503
						fclose($fp);
5504
					return fetch_web_data($location, $post_data, $keep_alive, $redirection_level + 1);
5505
				}
5506
			}
5507
5508
			// Make sure we get a 200 OK.
5509
			elseif (preg_match('~^HTTP/\S+\s+20[01]~i', $response) === 0)
5510
				return false;
5511
5512
			// Skip the headers...
5513
			while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
5514
			{
5515
				if (preg_match('~content-length:\s*(\d+)~i', $header, $match) != 0)
5516
					$content_length = $match[1];
5517
				elseif (preg_match('~connection:\s*close~i', $header) != 0)
5518
				{
5519
					$keep_alive_dom = null;
5520
					$keep_alive = false;
5521
				}
5522
5523
				continue;
5524
			}
5525
5526
			$data = '';
5527
			if (isset($content_length))
5528
			{
5529
				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...
5530
					$data .= fread($fp, $content_length - strlen($data));
5531
			}
5532
			else
5533
			{
5534
				while (!feof($fp))
5535
					$data .= fread($fp, 4096);
5536
			}
5537
5538
			if (!$keep_alive)
5539
				fclose($fp);
5540
		}
5541
5542
		// If using fsockopen didn't work, try to use cURL if available.
5543
		elseif (function_exists('curl_init'))
5544
		{
5545
			// Include the file containing the curl_fetch_web_data class.
5546
			require_once($sourcedir . '/Class-CurlFetchWeb.php');
5547
5548
			$fetch_data = new curl_fetch_web_data();
5549
			$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

5549
			$fetch_data->get_url_data($url, /** @scrutinizer ignore-type */ $post_data);
Loading history...
5550
5551
			// no errors and a 200 result, then we have a good dataset, well we at least have data. ;)
5552
			if ($fetch_data->result('code') == 200 && !$fetch_data->result('error'))
5553
				$data = $fetch_data->result('body');
5554
			else
5555
				return false;
5556
		}
5557
5558
		// Neither fsockopen nor curl are available. Well, phooey.
5559
		else
5560
			return false;
5561
	}
5562
	else
5563
	{
5564
		// Umm, this shouldn't happen?
5565
		trigger_error('fetch_web_data(): Bad URL', E_USER_NOTICE);
5566
		$data = false;
5567
	}
5568
5569
	return $data;
5570
}
5571
5572
/**
5573
 * Attempts to determine the MIME type of some data or a file.
5574
 *
5575
 * @param string $data The data to check, or the path or URL of a file to check.
5576
 * @param string $is_path If true, $data is a path or URL to a file.
5577
 * @return string|bool A MIME type, or false if we cannot determine it.
5578
 */
5579
function get_mime_type($data, $is_path = false)
5580
{
5581
	global $cachedir;
5582
5583
	$finfo_loaded = extension_loaded('fileinfo');
5584
	$exif_loaded = extension_loaded('exif') && function_exists('image_type_to_mime_type');
5585
5586
	// Oh well. We tried.
5587
	if (!$finfo_loaded && !$exif_loaded)
5588
		return false;
5589
5590
	// Start with the 'empty' MIME type.
5591
	$mime_type = 'application/x-empty';
5592
5593
	if ($finfo_loaded)
5594
	{
5595
		// Just some nice, simple data to analyze.
5596
		if (empty($is_path))
5597
			$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
5598
5599
		// A file, or maybe a URL?
5600
		else
5601
		{
5602
			// Local file.
5603
			if (file_exists($data))
5604
				$mime_type = mime_content_type($data);
5605
5606
			// URL.
5607
			elseif ($data = fetch_web_data($data))
5608
				$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
5609
		}
5610
	}
5611
	// Workaround using Exif requires a local file.
5612
	else
5613
	{
5614
		// If $data is a URL to fetch, do so.
5615
		if (!empty($is_path) && !file_exists($data) && url_exists($data))
5616
		{
5617
			$data = fetch_web_data($data);
5618
			$is_path = false;
5619
		}
5620
5621
		// If we don't have a local file, create one and use it.
5622
		if (empty($is_path))
5623
		{
5624
			$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 $str 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

5624
			$temp_file = tempnam($cachedir, md5(/** @scrutinizer ignore-type */ $data));
Loading history...
5625
			file_put_contents($temp_file, $data);
5626
			$is_path = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $is_path is dead and can be removed.
Loading history...
5627
			$data = $temp_file;
5628
		}
5629
5630
		$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

5630
		$imagetype = @exif_imagetype(/** @scrutinizer ignore-type */ $data);
Loading history...
5631
5632
		if (isset($temp_file))
5633
			unlink($temp_file);
5634
5635
		// Unfortunately, this workaround only works for image files.
5636
		if ($imagetype !== false)
5637
			$mime_type = image_type_to_mime_type($imagetype);
5638
	}
5639
5640
	return $mime_type;
5641
}
5642
5643
/**
5644
 * Checks whether a file or data has the expected MIME type.
5645
 *
5646
 * @param string $data The data to check, or the path or URL of a file to check.
5647
 * @param string $type_pattern A regex pattern to match the acceptable MIME types.
5648
 * @param string $is_path If true, $data is a path or URL to a file.
5649
 * @return int 1 if the detected MIME type matches the pattern, 0 if it doesn't, or 2 if we can't check.
5650
 */
5651
function check_mime_type($data, $type_pattern, $is_path = false)
5652
{
5653
	// Get the MIME type.
5654
	$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

5654
	$mime_type = get_mime_type($data, /** @scrutinizer ignore-type */ $is_path);
Loading history...
5655
5656
	// Couldn't determine it.
5657
	if ($mime_type === false)
5658
		return 2;
5659
5660
	// Check whether the MIME type matches expectations.
5661
	return (int) @preg_match('~' . $type_pattern . '~', $mime_type);
5662
}
5663
5664
/**
5665
 * Prepares an array of "likes" info for the topic specified by $topic
5666
 *
5667
 * @param integer $topic The topic ID to fetch the info from.
5668
 * @return array An array of IDs of messages in the specified topic that the current user likes
5669
 */
5670
function prepareLikesContext($topic)
5671
{
5672
	global $user_info, $smcFunc;
5673
5674
	// Make sure we have something to work with.
5675
	if (empty($topic))
5676
		return array();
5677
5678
	// We already know the number of likes per message, we just want to know whether the current user liked it or not.
5679
	$user = $user_info['id'];
5680
	$cache_key = 'likes_topic_' . $topic . '_' . $user;
5681
	$ttl = 180;
5682
5683
	if (($temp = cache_get_data($cache_key, $ttl)) === null)
5684
	{
5685
		$temp = array();
5686
		$request = $smcFunc['db_query']('', '
5687
			SELECT content_id
5688
			FROM {db_prefix}user_likes AS l
5689
				INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg)
5690
			WHERE l.id_member = {int:current_user}
5691
				AND l.content_type = {literal:msg}
5692
				AND m.id_topic = {int:topic}',
5693
			array(
5694
				'current_user' => $user,
5695
				'topic' => $topic,
5696
			)
5697
		);
5698
		while ($row = $smcFunc['db_fetch_assoc']($request))
5699
			$temp[] = (int) $row['content_id'];
5700
5701
		cache_put_data($cache_key, $temp, $ttl);
5702
	}
5703
5704
	return $temp;
5705
}
5706
5707
/**
5708
 * Microsoft uses their own character set Code Page 1252 (CP1252), which is a
5709
 * superset of ISO 8859-1, defining several characters between DEC 128 and 159
5710
 * that are not normally displayable.  This converts the popular ones that
5711
 * appear from a cut and paste from windows.
5712
 *
5713
 * @param string $string The string
5714
 * @return string The sanitized string
5715
 */
5716
function sanitizeMSCutPaste($string)
5717
{
5718
	global $context;
5719
5720
	if (empty($string))
5721
		return $string;
5722
5723
	// UTF-8 occurences of MS special characters
5724
	$findchars_utf8 = array(
5725
		"\xe2\x80\x9a",	// single low-9 quotation mark
5726
		"\xe2\x80\x9e",	// double low-9 quotation mark
5727
		"\xe2\x80\xa6",	// horizontal ellipsis
5728
		"\xe2\x80\x98",	// left single curly quote
5729
		"\xe2\x80\x99",	// right single curly quote
5730
		"\xe2\x80\x9c",	// left double curly quote
5731
		"\xe2\x80\x9d",	// right double curly quote
5732
	);
5733
5734
	// windows 1252 / iso equivalents
5735
	$findchars_iso = array(
5736
		chr(130),
5737
		chr(132),
5738
		chr(133),
5739
		chr(145),
5740
		chr(146),
5741
		chr(147),
5742
		chr(148),
5743
	);
5744
5745
	// safe replacements
5746
	$replacechars = array(
5747
		',',	// &sbquo;
5748
		',,',	// &bdquo;
5749
		'...',	// &hellip;
5750
		"'",	// &lsquo;
5751
		"'",	// &rsquo;
5752
		'"',	// &ldquo;
5753
		'"',	// &rdquo;
5754
	);
5755
5756
	if ($context['utf8'])
5757
		$string = str_replace($findchars_utf8, $replacechars, $string);
5758
	else
5759
		$string = str_replace($findchars_iso, $replacechars, $string);
5760
5761
	return $string;
5762
}
5763
5764
/**
5765
 * Decode numeric html entities to their ascii or UTF8 equivalent character.
5766
 *
5767
 * Callback function for preg_replace_callback in subs-members
5768
 * Uses capture group 2 in the supplied array
5769
 * Does basic scan to ensure characters are inside a valid range
5770
 *
5771
 * @param array $matches An array of matches (relevant info should be the 3rd item)
5772
 * @return string A fixed string
5773
 */
5774
function replaceEntities__callback($matches)
5775
{
5776
	global $context;
5777
5778
	if (!isset($matches[2]))
5779
		return '';
5780
5781
	$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

5781
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5782
5783
	// remove left to right / right to left overrides
5784
	if ($num === 0x202D || $num === 0x202E)
5785
		return '';
5786
5787
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
5788
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
5789
		return '&#' . $num . ';';
5790
5791
	if (empty($context['utf8']))
5792
	{
5793
		// no control characters
5794
		if ($num < 0x20)
5795
			return '';
5796
		// text is text
5797
		elseif ($num < 0x80)
5798
			return chr($num);
0 ignored issues
show
Bug introduced by
It seems like $num can also be of type double; however, parameter $ascii 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

5798
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5799
		// all others get html-ised
5800
		else
5801
			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

5801
			return '&#' . /** @scrutinizer ignore-type */ $matches[2] . ';';
Loading history...
5802
	}
5803
	else
5804
	{
5805
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
5806
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
5807
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
5808
			return '';
5809
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
5810
		elseif ($num < 0x80)
5811
			return chr($num);
5812
		// <0x800 (2048)
5813
		elseif ($num < 0x800)
5814
			return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5815
		// < 0x10000 (65536)
5816
		elseif ($num < 0x10000)
5817
			return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5818
		// <= 0x10FFFF (1114111)
5819
		else
5820
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5821
	}
5822
}
5823
5824
/**
5825
 * Converts html entities to utf8 equivalents
5826
 *
5827
 * Callback function for preg_replace_callback
5828
 * Uses capture group 1 in the supplied array
5829
 * Does basic checks to keep characters inside a viewable range.
5830
 *
5831
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
5832
 * @return string The fixed string
5833
 */
5834
function fixchar__callback($matches)
5835
{
5836
	if (!isset($matches[1]))
5837
		return '';
5838
5839
	$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

5839
	$num = $matches[1][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[1], 1)) : (int) $matches[1];
Loading history...
5840
5841
	// <0x20 are control characters, > 0x10FFFF is past the end of the utf8 character set
5842
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text), 0x202D-E are left to right overrides
5843
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202D || $num === 0x202E)
5844
		return '';
5845
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
5846
	elseif ($num < 0x80)
5847
		return chr($num);
0 ignored issues
show
Bug introduced by
It seems like $num can also be of type double; however, parameter $ascii 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

5847
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5848
	// <0x800 (2048)
5849
	elseif ($num < 0x800)
5850
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5851
	// < 0x10000 (65536)
5852
	elseif ($num < 0x10000)
5853
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5854
	// <= 0x10FFFF (1114111)
5855
	else
5856
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5857
}
5858
5859
/**
5860
 * Strips out invalid html entities, replaces others with html style &#123; codes
5861
 *
5862
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
5863
 * strpos, strlen, substr etc
5864
 *
5865
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
5866
 * @return string The fixed string
5867
 */
5868
function entity_fix__callback($matches)
5869
{
5870
	if (!isset($matches[2]))
5871
		return '';
5872
5873
	$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

5873
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5874
5875
	// we don't allow control characters, characters out of range, byte markers, etc
5876
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
5877
		return '';
5878
	else
5879
		return '&#' . $num . ';';
5880
}
5881
5882
/**
5883
 * Return a Gravatar URL based on
5884
 * - the supplied email address,
5885
 * - the global maximum rating,
5886
 * - the global default fallback,
5887
 * - maximum sizes as set in the admin panel.
5888
 *
5889
 * It is SSL aware, and caches most of the parameters.
5890
 *
5891
 * @param string $email_address The user's email address
5892
 * @return string The gravatar URL
5893
 */
5894
function get_gravatar_url($email_address)
5895
{
5896
	global $modSettings, $smcFunc;
5897
	static $url_params = null;
5898
5899
	if ($url_params === null)
5900
	{
5901
		$ratings = array('G', 'PG', 'R', 'X');
5902
		$defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank');
5903
		$url_params = array();
5904
		if (!empty($modSettings['gravatarMaxRating']) && in_array($modSettings['gravatarMaxRating'], $ratings))
5905
			$url_params[] = 'rating=' . $modSettings['gravatarMaxRating'];
5906
		if (!empty($modSettings['gravatarDefault']) && in_array($modSettings['gravatarDefault'], $defaults))
5907
			$url_params[] = 'default=' . $modSettings['gravatarDefault'];
5908
		if (!empty($modSettings['avatar_max_width_external']))
5909
			$size_string = (int) $modSettings['avatar_max_width_external'];
5910
		if (!empty($modSettings['avatar_max_height_external']) && !empty($size_string))
5911
			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...
5912
				$size_string = $modSettings['avatar_max_height_external'];
5913
5914
		if (!empty($size_string))
5915
			$url_params[] = 's=' . $size_string;
5916
	}
5917
	$http_method = !empty($modSettings['force_ssl']) ? 'https://secure' : 'http://www';
5918
5919
	return $http_method . '.gravatar.com/avatar/' . md5($smcFunc['strtolower']($email_address)) . '?' . implode('&', $url_params);
5920
}
5921
5922
/**
5923
 * Get a list of timezones.
5924
 *
5925
 * @param string $when An optional date or time for which to calculate the timezone offset values. May be a Unix timestamp or any string that strtotime() can understand. Defaults to 'now'.
5926
 * @return array An array of timezone info.
5927
 */
5928
function smf_list_timezones($when = 'now')
5929
{
5930
	global $modSettings, $tztxt, $txt, $context, $cur_profile, $sourcedir;
5931
	static $timezones_when = array();
5932
5933
	require_once($sourcedir . '/Subs-Timezones.php');
5934
5935
	// Parseable datetime string?
5936
	if (is_int($timestamp = strtotime($when)))
0 ignored issues
show
introduced by
The condition is_int($timestamp = strtotime($when)) is always true.
Loading history...
5937
		$when = $timestamp;
5938
5939
	// A Unix timestamp?
5940
	elseif (is_numeric($when))
5941
		$when = intval($when);
5942
5943
	// Invalid value? Just get current Unix timestamp.
5944
	else
5945
		$when = time();
5946
5947
	// No point doing this over if we already did it once
5948
	if (isset($timezones_when[$when]))
5949
		return $timezones_when[$when];
5950
5951
	// We'll need these too
5952
	$date_when = date_create('@' . $when);
5953
	$later = strtotime('@' . $when . ' + 1 year');
5954
5955
	// Load up any custom time zone descriptions we might have
5956
	loadLanguage('Timezones');
5957
5958
	$tzid_metazones = get_tzid_metazones();
5959
5960
	// Should we put time zones from certain countries at the top of the list?
5961
	$priority_countries = !empty($modSettings['timezone_priority_countries']) ? explode(',', $modSettings['timezone_priority_countries']) : array();
5962
5963
	$priority_tzids = array();
5964
	foreach ($priority_countries as $country)
5965
	{
5966
		$country_tzids = get_sorted_tzids_for_country($country);
5967
5968
		if (!empty($country_tzids))
5969
			$priority_tzids = array_merge($priority_tzids, $country_tzids);
5970
	}
5971
5972
	// Antarctic research stations should be listed last, unless you're running a penguin forum
5973
	$low_priority_tzids = !in_array('AQ', $priority_countries) ? timezone_identifiers_list(DateTimeZone::ANTARCTICA) : array();
5974
5975
	$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
It seems like $low_priority_tzids can also be of type false; however, parameter $_ of array_diff() does only seem to accept array, 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

5975
	$normal_priority_tzids = array_diff(array_unique(array_merge(array_keys($tzid_metazones), timezone_identifiers_list())), $priority_tzids, /** @scrutinizer ignore-type */ $low_priority_tzids);
Loading history...
Bug introduced by
It seems like timezone_identifiers_list() can also be of type false; however, parameter $array2 of array_merge() does only seem to accept array|null, 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

5975
	$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...
5976
5977
	// Process the preferred timezones first, then the normal ones, then the low priority ones.
5978
	$tzids = array_merge($priority_tzids, array('UTC'), $normal_priority_tzids, $low_priority_tzids);
0 ignored issues
show
Bug introduced by
It seems like $low_priority_tzids can also be of type false; however, parameter $_ of array_merge() does only seem to accept array, 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

5978
	$tzids = array_merge($priority_tzids, array('UTC'), $normal_priority_tzids, /** @scrutinizer ignore-type */ $low_priority_tzids);
Loading history...
5979
5980
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
5981
	$dst_types = array();
5982
	$labels = array();
5983
	$offsets = array();
5984
	foreach ($tzids as $tzid)
5985
	{
5986
		// We don't want UTC right now
5987
		if ($tzid == 'UTC')
5988
			continue;
5989
5990
		$tz = timezone_open($tzid);
5991
5992
		// First, get the set of transition rules for this tzid
5993
		$tzinfo = timezone_transitions_get($tz, $when, $later);
0 ignored issues
show
Bug introduced by
It seems like $tz can also be of type false; however, parameter $object of timezone_transitions_get() does only seem to accept DateTimeZone, 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

5993
		$tzinfo = timezone_transitions_get(/** @scrutinizer ignore-type */ $tz, $when, $later);
Loading history...
5994
5995
		// Use the entire set of transition rules as the array *key* so we can avoid duplicates
5996
		$tzkey = serialize($tzinfo);
5997
5998
		// ...But make sure to include all explicitly defined meta-zones.
5999
		if (isset($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid]))
6000
			$tzkey = serialize(array_merge($tzinfo, array('metazone' => $tzid_metazones[$tzid])));
0 ignored issues
show
Bug introduced by
It seems like $tzinfo can also be of type false; however, parameter $array1 of array_merge() does only seem to accept array, 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

6000
			$tzkey = serialize(array_merge(/** @scrutinizer ignore-type */ $tzinfo, array('metazone' => $tzid_metazones[$tzid])));
Loading history...
6001
6002
		// Don't overwrite our preferred tzids
6003
		if (empty($zones[$tzkey]['tzid']))
6004
		{
6005
			$zones[$tzkey]['tzid'] = $tzid;
6006
			$zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
0 ignored issues
show
Bug introduced by
It seems like $tzinfo can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, 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

6006
			$zones[$tzkey]['dst_type'] = count(/** @scrutinizer ignore-type */ $tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
Loading history...
6007
6008
			foreach ($tzinfo as $transition) {
6009
				$zones[$tzkey]['abbrs'][] = $transition['abbr'];
6010
			}
6011
6012
			if (isset($tzid_metazones[$tzid]))
6013
				$zones[$tzkey]['metazone'] = $tzid_metazones[$tzid];
6014
			else
6015
			{
6016
				$tzgeo = timezone_location_get($tz);
0 ignored issues
show
Bug introduced by
It seems like $tz can also be of type false; however, parameter $object of timezone_location_get() does only seem to accept DateTimeZone, 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

6016
				$tzgeo = timezone_location_get(/** @scrutinizer ignore-type */ $tz);
Loading history...
6017
				$country_tzids = get_sorted_tzids_for_country($tzgeo['country_code']);
6018
6019
				if (count($country_tzids) === 1)
6020
					$zones[$tzkey]['metazone'] = $txt['iso3166'][$tzgeo['country_code']];
6021
			}
6022
		}
6023
6024
		// A time zone from a prioritized country?
6025
		if (in_array($tzid, $priority_tzids))
6026
			$priority_zones[$tzkey] = true;
6027
6028
		// Keep track of the location and offset for this tzid
6029
		if (!empty($txt[$tzid]))
6030
			$zones[$tzkey]['locations'][] = $txt[$tzid];
6031
		else
6032
		{
6033
			$tzid_parts = explode('/', $tzid);
6034
			$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
6035
		}
6036
		$offsets[$tzkey] = $tzinfo[0]['offset'];
6037
6038
		// Figure out the "metazone" info for the label
6039
		if (empty($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid]))
6040
		{
6041
			$zones[$tzkey]['metazone'] = $tzid_metazones[$tzid];
6042
			$zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
6043
		}
6044
		$dst_types[$tzkey] = count($tzinfo) > 1 ? 'c' : ($tzinfo[0]['isdst'] ? 't' : 'f');
6045
		$labels[$tzkey] = !empty($zones[$tzkey]['metazone']) && !empty($tztxt[$zones[$tzkey]['metazone']]) ? $tztxt[$zones[$tzkey]['metazone']] : '';
6046
6047
		// Remember this for later
6048
		if (isset($cur_profile['timezone']) && $cur_profile['timezone'] == $tzid)
6049
			$member_tzkey = $tzkey;
6050
		if (isset($context['event']['tz']) && $context['event']['tz'] == $tzid)
6051
			$event_tzkey = $tzkey;
6052
	}
6053
6054
	// Sort by offset, then label, then DST type.
6055
	array_multisort($offsets, SORT_ASC, SORT_NUMERIC, $labels, SORT_ASC, $dst_types, SORT_ASC, $zones);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $zones seems to be defined by a foreach iteration on line 5984. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
6056
6057
	// Build the final array of formatted values
6058
	$priority_timezones = array();
6059
	$timezones = array();
6060
	foreach ($zones as $tzkey => $tzvalue)
6061
	{
6062
		date_timezone_set($date_when, timezone_open($tzvalue['tzid']));
0 ignored issues
show
Bug introduced by
It seems like $date_when can also be of type false; however, parameter $object of date_timezone_set() does only seem to accept DateTime, 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

6062
		date_timezone_set(/** @scrutinizer ignore-type */ $date_when, timezone_open($tzvalue['tzid']));
Loading history...
Bug introduced by
It seems like timezone_open($tzvalue['tzid']) can also be of type false; however, parameter $timezone of date_timezone_set() does only seem to accept DateTimeZone, 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

6062
		date_timezone_set($date_when, /** @scrutinizer ignore-type */ timezone_open($tzvalue['tzid']));
Loading history...
6063
6064
		// Use the human friendly time zone name, if there is one.
6065
		$desc = '';
6066
		if (!empty($tzvalue['metazone']))
6067
		{
6068
			if (!empty($tztxt[$tzvalue['metazone']]))
6069
				$metazone = $tztxt[$tzvalue['metazone']];
6070
			else
6071
				$metazone = sprintf($tztxt['generic_timezone'], $tzvalue['metazone'], '%1$s');
6072
6073
			switch ($tzvalue['dst_type'])
6074
			{
6075
				case 0:
6076
					$desc = sprintf($metazone, $tztxt['daylight_saving_time_false']);
6077
					break;
6078
6079
				case 1:
6080
					$desc = sprintf($metazone, '');
6081
					break;
6082
6083
				case 2:
6084
					$desc = sprintf($metazone, $tztxt['daylight_saving_time_true']);
6085
					break;
6086
			}
6087
		}
6088
		// Otherwise, use the list of locations (max 5, so things don't get silly)
6089
		else
6090
			$desc = implode(', ', array_slice(array_unique($tzvalue['locations']), 0, 5)) . (count($tzvalue['locations']) > 5 ? ', ' . $txt['etc'] : '');
6091
6092
		// We don't want abbreviations like '+03' or '-11'.
6093
		$abbrs = array_filter($tzvalue['abbrs'], function ($abbr) {
6094
			return !strspn($abbr, '+-');
6095
		});
6096
		$abbrs = count($abbrs) == count($tzvalue['abbrs']) ? array_unique($abbrs) : array();
6097
6098
		// Show the UTC offset and abbreviation(s).
6099
		$desc = '[UTC' . date_format($date_when, 'P') . '] - ' . str_replace('  ', ' ', $desc) . (!empty($abbrs) ? ' (' . implode('/', $abbrs) . ')' : '');
6100
6101
		if (isset($priority_zones[$tzkey]))
6102
			$priority_timezones[$tzvalue['tzid']] = $desc;
6103
		else
6104
			$timezones[$tzvalue['tzid']] = $desc;
6105
6106
		// Automatically fix orphaned time zones.
6107
		if (isset($member_tzkey) && $member_tzkey == $tzkey)
6108
			$cur_profile['timezone'] = $tzvalue['tzid'];
6109
		if (isset($event_tzkey) && $event_tzkey == $tzkey)
6110
			$context['event']['tz'] = $tzvalue['tzid'];
6111
	}
6112
6113
	if (!empty($priority_timezones))
6114
		$priority_timezones[] = '-----';
6115
6116
	$timezones = array_merge(
6117
		$priority_timezones,
6118
		array('UTC' => 'UTC' . (!empty($tztxt['UTC']) ? ' - ' . $tztxt['UTC'] : ''), '-----'),
6119
		$timezones
6120
	);
6121
6122
	$timezones_when[$when] = $timezones;
6123
6124
	return $timezones_when[$when];
6125
}
6126
6127
/**
6128
 * Gets a member's selected timezone identifier
6129
 *
6130
 * @param int $id_member The member id to look up. If not provided, the current user's id will be used.
6131
 * @return string The timezone identifier string for the user's timezone.
6132
 */
6133
function getUserTimezone($id_member = null)
6134
{
6135
	global $smcFunc, $context, $user_info, $modSettings, $user_settings;
6136
	static $member_cache = array();
6137
6138
	if (is_null($id_member) && $user_info['is_guest'] == false)
6139
		$id_member = $context['user']['id'];
6140
6141
	// Did we already look this up?
6142
	if (isset($id_member) && isset($member_cache[$id_member]))
6143
	{
6144
		return $member_cache[$id_member];
6145
	}
6146
6147
	// Check if we already have this in $user_settings.
6148
	if (isset($user_settings['id_member']) && $user_settings['id_member'] == $id_member && !empty($user_settings['timezone']))
6149
	{
6150
		$member_cache[$id_member] = $user_settings['timezone'];
6151
		return $user_settings['timezone'];
6152
	}
6153
6154
	// Look it up in the database.
6155
	if (isset($id_member))
6156
	{
6157
		$request = $smcFunc['db_query']('', '
6158
			SELECT timezone
6159
			FROM {db_prefix}members
6160
			WHERE id_member = {int:id_member}',
6161
			array(
6162
				'id_member' => $id_member,
6163
			)
6164
		);
6165
		list($timezone) = $smcFunc['db_fetch_row']($request);
6166
		$smcFunc['db_free_result']($request);
6167
	}
6168
6169
	// If it is invalid, fall back to the default.
6170
	if (empty($timezone) || !in_array($timezone, timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)))
0 ignored issues
show
Bug introduced by
It seems like timezone_identifiers_lis...eTimeZone::ALL_WITH_BC) can also be of type false; however, parameter $haystack of in_array() does only seem to accept array, 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

6170
	if (empty($timezone) || !in_array($timezone, /** @scrutinizer ignore-type */ timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)))
Loading history...
6171
		$timezone = isset($modSettings['default_timezone']) ? $modSettings['default_timezone'] : date_default_timezone_get();
6172
6173
	// Save for later.
6174
	if (isset($id_member))
6175
		$member_cache[$id_member] = $timezone;
6176
6177
	return $timezone;
6178
}
6179
6180
/**
6181
 * Converts an IP address into binary
6182
 *
6183
 * @param string $ip_address An IP address in IPv4, IPv6 or decimal notation
6184
 * @return string|false The IP address in binary or false
6185
 */
6186
function inet_ptod($ip_address)
6187
{
6188
	if (!isValidIP($ip_address))
6189
		return $ip_address;
6190
6191
	$bin = inet_pton($ip_address);
6192
	return $bin;
6193
}
6194
6195
/**
6196
 * Converts a binary version of an IP address into a readable format
6197
 *
6198
 * @param string $bin An IP address in IPv4, IPv6 (Either string (postgresql) or binary (other databases))
6199
 * @return string|false The IP address in presentation format or false on error
6200
 */
6201
function inet_dtop($bin)
6202
{
6203
	global $db_type;
6204
6205
	if (empty($bin))
6206
		return '';
6207
	elseif ($db_type == 'postgresql')
6208
		return $bin;
6209
	// Already a String?
6210
	elseif (isValidIP($bin))
6211
		return $bin;
6212
	return inet_ntop($bin);
6213
}
6214
6215
/**
6216
 * Safe serialize() and unserialize() replacements
6217
 *
6218
 * @license Public Domain
6219
 *
6220
 * @author anthon (dot) pang (at) gmail (dot) com
6221
 */
6222
6223
/**
6224
 * Safe serialize() replacement. Recursive
6225
 * - output a strict subset of PHP's native serialized representation
6226
 * - does not serialize objects
6227
 *
6228
 * @param mixed $value
6229
 * @return string
6230
 */
6231
function _safe_serialize($value)
6232
{
6233
	if (is_null($value))
6234
		return 'N;';
6235
6236
	if (is_bool($value))
6237
		return 'b:' . (int) $value . ';';
6238
6239
	if (is_int($value))
6240
		return 'i:' . $value . ';';
6241
6242
	if (is_float($value))
6243
		return 'd:' . str_replace(',', '.', $value) . ';';
6244
6245
	if (is_string($value))
6246
		return 's:' . strlen($value) . ':"' . $value . '";';
6247
6248
	if (is_array($value))
6249
	{
6250
		$out = '';
6251
		foreach ($value as $k => $v)
6252
			$out .= _safe_serialize($k) . _safe_serialize($v);
6253
6254
		return 'a:' . count($value) . ':{' . $out . '}';
6255
	}
6256
6257
	// safe_serialize cannot serialize resources or objects.
6258
	return false;
6259
}
6260
6261
/**
6262
 * Wrapper for _safe_serialize() that handles exceptions and multibyte encoding issues.
6263
 *
6264
 * @param mixed $value
6265
 * @return string
6266
 */
6267
function safe_serialize($value)
6268
{
6269
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
6270
	if (function_exists('mb_internal_encoding') &&
6271
		(((int) ini_get('mbstring.func_overload')) & 2))
6272
	{
6273
		$mbIntEnc = mb_internal_encoding();
6274
		mb_internal_encoding('ASCII');
6275
	}
6276
6277
	$out = _safe_serialize($value);
6278
6279
	if (isset($mbIntEnc))
6280
		mb_internal_encoding($mbIntEnc);
6281
6282
	return $out;
6283
}
6284
6285
/**
6286
 * Safe unserialize() replacement
6287
 * - accepts a strict subset of PHP's native serialized representation
6288
 * - does not unserialize objects
6289
 *
6290
 * @param string $str
6291
 * @return mixed
6292
 * @throw Exception if $str is malformed or contains unsupported types (e.g., resources, objects)
6293
 */
6294
function _safe_unserialize($str)
6295
{
6296
	// Input  is not a string.
6297
	if (empty($str) || !is_string($str))
6298
		return false;
6299
6300
	$stack = array();
6301
	$expected = array();
6302
6303
	/*
6304
	 * states:
6305
	 *   0 - initial state, expecting a single value or array
6306
	 *   1 - terminal state
6307
	 *   2 - in array, expecting end of array or a key
6308
	 *   3 - in array, expecting value or another array
6309
	 */
6310
	$state = 0;
6311
	while ($state != 1)
6312
	{
6313
		$type = isset($str[0]) ? $str[0] : '';
6314
		if ($type == '}')
6315
			$str = substr($str, 1);
6316
6317
		elseif ($type == 'N' && $str[1] == ';')
6318
		{
6319
			$value = null;
6320
			$str = substr($str, 2);
6321
		}
6322
		elseif ($type == 'b' && preg_match('/^b:([01]);/', $str, $matches))
6323
		{
6324
			$value = $matches[1] == '1' ? true : false;
6325
			$str = substr($str, 4);
6326
		}
6327
		elseif ($type == 'i' && preg_match('/^i:(-?[0-9]+);(.*)/s', $str, $matches))
6328
		{
6329
			$value = (int) $matches[1];
6330
			$str = $matches[2];
6331
		}
6332
		elseif ($type == 'd' && preg_match('/^d:(-?[0-9]+\.?[0-9]*(E[+-][0-9]+)?);(.*)/s', $str, $matches))
6333
		{
6334
			$value = (float) $matches[1];
6335
			$str = $matches[3];
6336
		}
6337
		elseif ($type == 's' && preg_match('/^s:([0-9]+):"(.*)/s', $str, $matches) && substr($matches[2], (int) $matches[1], 2) == '";')
6338
		{
6339
			$value = substr($matches[2], 0, (int) $matches[1]);
6340
			$str = substr($matches[2], (int) $matches[1] + 2);
6341
		}
6342
		elseif ($type == 'a' && preg_match('/^a:([0-9]+):{(.*)/s', $str, $matches))
6343
		{
6344
			$expectedLength = (int) $matches[1];
6345
			$str = $matches[2];
6346
		}
6347
6348
		// Object or unknown/malformed type.
6349
		else
6350
			return false;
6351
6352
		switch ($state)
6353
		{
6354
			case 3: // In array, expecting value or another array.
6355
				if ($type == 'a')
6356
				{
6357
					$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...
6358
					$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...
6359
					$list = &$list[$key];
6360
					$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...
6361
					$state = 2;
6362
					break;
6363
				}
6364
				if ($type != '}')
6365
				{
6366
					$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...
6367
					$state = 2;
6368
					break;
6369
				}
6370
6371
				// Missing array value.
6372
				return false;
6373
6374
			case 2: // in array, expecting end of array or a key
6375
				if ($type == '}')
6376
				{
6377
					// Array size is less than expected.
6378
					if (count($list) < end($expected))
6379
						return false;
6380
6381
					unset($list);
6382
					$list = &$stack[count($stack) - 1];
6383
					array_pop($stack);
6384
6385
					// Go to terminal state if we're at the end of the root array.
6386
					array_pop($expected);
6387
6388
					if (count($expected) == 0)
6389
						$state = 1;
6390
6391
					break;
6392
				}
6393
6394
				if ($type == 'i' || $type == 's')
6395
				{
6396
					// Array size exceeds expected length.
6397
					if (count($list) >= end($expected))
6398
						return false;
6399
6400
					$key = $value;
6401
					$state = 3;
6402
					break;
6403
				}
6404
6405
				// Illegal array index type.
6406
				return false;
6407
6408
			// Expecting array or value.
6409
			case 0:
6410
				if ($type == 'a')
6411
				{
6412
					$data = array();
6413
					$list = &$data;
6414
					$expected[] = $expectedLength;
6415
					$state = 2;
6416
					break;
6417
				}
6418
6419
				if ($type != '}')
6420
				{
6421
					$data = $value;
6422
					$state = 1;
6423
					break;
6424
				}
6425
6426
				// Not in array.
6427
				return false;
6428
		}
6429
	}
6430
6431
	// Trailing data in input.
6432
	if (!empty($str))
6433
		return false;
6434
6435
	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...
6436
}
6437
6438
/**
6439
 * Wrapper for _safe_unserialize() that handles exceptions and multibyte encoding issue
6440
 *
6441
 * @param string $str
6442
 * @return mixed
6443
 */
6444
function safe_unserialize($str)
6445
{
6446
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
6447
	if (function_exists('mb_internal_encoding') &&
6448
		(((int) ini_get('mbstring.func_overload')) & 0x02))
6449
	{
6450
		$mbIntEnc = mb_internal_encoding();
6451
		mb_internal_encoding('ASCII');
6452
	}
6453
6454
	$out = _safe_unserialize($str);
6455
6456
	if (isset($mbIntEnc))
6457
		mb_internal_encoding($mbIntEnc);
6458
6459
	return $out;
6460
}
6461
6462
/**
6463
 * Tries different modes to make file/dirs writable. Wrapper function for chmod()
6464
 *
6465
 * @param string $file The file/dir full path.
6466
 * @param int $value Not needed, added for legacy reasons.
6467
 * @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.
6468
 */
6469
function smf_chmod($file, $value = 0)
6470
{
6471
	// No file? no checks!
6472
	if (empty($file))
6473
		return false;
6474
6475
	// Already writable?
6476
	if (is_writable($file))
6477
		return true;
6478
6479
	// Do we have a file or a dir?
6480
	$isDir = is_dir($file);
6481
	$isWritable = false;
6482
6483
	// Set different modes.
6484
	$chmodValues = $isDir ? array(0750, 0755, 0775, 0777) : array(0644, 0664, 0666);
6485
6486
	foreach ($chmodValues as $val)
6487
	{
6488
		// If it's writable, break out of the loop.
6489
		if (is_writable($file))
6490
		{
6491
			$isWritable = true;
6492
			break;
6493
		}
6494
6495
		else
6496
			@chmod($file, $val);
6497
	}
6498
6499
	return $isWritable;
6500
}
6501
6502
/**
6503
 * Wrapper function for json_decode() with error handling.
6504
 *
6505
 * @param string $json The string to decode.
6506
 * @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.
6507
 * @param bool $logIt To specify if the error will be logged if theres any.
6508
 * @return array Either an empty array or the decoded data as an array.
6509
 */
6510
function smf_json_decode($json, $returnAsArray = false, $logIt = true)
6511
{
6512
	global $txt;
6513
6514
	// Come on...
6515
	if (empty($json) || !is_string($json))
6516
		return array();
6517
6518
	$returnArray = @json_decode($json, $returnAsArray);
6519
6520
	// PHP 5.3 so no json_last_error_msg()
6521
	switch (json_last_error())
6522
	{
6523
		case JSON_ERROR_NONE:
6524
			$jsonError = false;
6525
			break;
6526
		case JSON_ERROR_DEPTH:
6527
			$jsonError = 'JSON_ERROR_DEPTH';
6528
			break;
6529
		case JSON_ERROR_STATE_MISMATCH:
6530
			$jsonError = 'JSON_ERROR_STATE_MISMATCH';
6531
			break;
6532
		case JSON_ERROR_CTRL_CHAR:
6533
			$jsonError = 'JSON_ERROR_CTRL_CHAR';
6534
			break;
6535
		case JSON_ERROR_SYNTAX:
6536
			$jsonError = 'JSON_ERROR_SYNTAX';
6537
			break;
6538
		case JSON_ERROR_UTF8:
6539
			$jsonError = 'JSON_ERROR_UTF8';
6540
			break;
6541
		default:
6542
			$jsonError = 'unknown';
6543
			break;
6544
	}
6545
6546
	// Something went wrong!
6547
	if (!empty($jsonError) && $logIt)
6548
	{
6549
		// Being a wrapper means we lost our smf_error_handler() privileges :(
6550
		$jsonDebug = debug_backtrace();
6551
		$jsonDebug = $jsonDebug[0];
6552
		loadLanguage('Errors');
6553
6554
		if (!empty($jsonDebug))
6555
			log_error($txt['json_' . $jsonError], 'critical', $jsonDebug['file'], $jsonDebug['line']);
6556
6557
		else
6558
			log_error($txt['json_' . $jsonError], 'critical');
6559
6560
		// Everyone expects an array.
6561
		return array();
6562
	}
6563
6564
	return $returnArray;
6565
}
6566
6567
/**
6568
 * Check the given String if he is a valid IPv4 or IPv6
6569
 * return true or false
6570
 *
6571
 * @param string $IPString
6572
 *
6573
 * @return bool
6574
 */
6575
function isValidIP($IPString)
6576
{
6577
	return filter_var($IPString, FILTER_VALIDATE_IP) !== false;
6578
}
6579
6580
/**
6581
 * Outputs a response.
6582
 * It assumes the data is already a string.
6583
 *
6584
 * @param string $data The data to print
6585
 * @param string $type The content type. Defaults to Json.
6586
 * @return void
6587
 */
6588
function smf_serverResponse($data = '', $type = 'content-type: application/json')
6589
{
6590
	global $db_show_debug, $modSettings;
6591
6592
	// Defensive programming anyone?
6593
	if (empty($data))
6594
		return false;
6595
6596
	// Don't need extra stuff...
6597
	$db_show_debug = false;
6598
6599
	// Kill anything else.
6600
	ob_end_clean();
6601
6602
	if (!empty($modSettings['CompressedOutput']))
6603
		@ob_start('ob_gzhandler');
6604
6605
	else
6606
		ob_start();
6607
6608
	// Set the header.
6609
	header($type);
6610
6611
	// Echo!
6612
	echo $data;
6613
6614
	// Done.
6615
	obExit(false);
6616
}
6617
6618
/**
6619
 * Creates an optimized regex to match all known top level domains.
6620
 *
6621
 * The optimized regex is stored in $modSettings['tld_regex'].
6622
 *
6623
 * To update the stored version of the regex to use the latest list of valid
6624
 * TLDs from iana.org, set the $update parameter to true. Updating can take some
6625
 * time, based on network connectivity, so it should normally only be done by
6626
 * calling this function from a background or scheduled task.
6627
 *
6628
 * If $update is not true, but the regex is missing or invalid, the regex will
6629
 * be regenerated from a hard-coded list of TLDs. This regenerated regex will be
6630
 * overwritten on the next scheduled update.
6631
 *
6632
 * @param bool $update If true, fetch and process the latest official list of TLDs from iana.org.
6633
 */
6634
function set_tld_regex($update = false)
6635
{
6636
	global $sourcedir, $smcFunc, $modSettings;
6637
	static $done = false;
6638
6639
	// If we don't need to do anything, don't
6640
	if (!$update && $done)
6641
		return;
6642
6643
	// Should we get a new copy of the official list of TLDs?
6644
	if ($update)
6645
	{
6646
		$tlds = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt');
6647
		$tlds_md5 = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt.md5');
6648
6649
		/**
6650
		 * If the Internet Assigned Numbers Authority can't be reached, the Internet is GONE!
6651
		 * We're probably running on a server hidden in a bunker deep underground to protect
6652
		 * it from marauding bandits roaming on the surface. We don't want to waste precious
6653
		 * electricity on pointlessly repeating background tasks, so we'll wait until the next
6654
		 * regularly scheduled update to see if civilization has been restored.
6655
		 */
6656
		if ($tlds === false || $tlds_md5 === false)
6657
			$postapocalypticNightmare = true;
6658
6659
		// Make sure nothing went horribly wrong along the way.
6660
		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 $str 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

6660
		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

6660
		if (md5($tlds) != substr(/** @scrutinizer ignore-type */ $tlds_md5, 0, 32))
Loading history...
6661
			$tlds = array();
6662
	}
6663
	// If we aren't updating and the regex is valid, we're done
6664
	elseif (!empty($modSettings['tld_regex']) && @preg_match('~' . $modSettings['tld_regex'] . '~', null) !== false)
6665
	{
6666
		$done = true;
6667
		return;
6668
	}
6669
6670
	// If we successfully got an update, process the list into an array
6671
	if (!empty($tlds))
6672
	{
6673
		// Clean $tlds and convert it to an array
6674
		$tlds = array_filter(explode("\n", strtolower($tlds)), function($line)
6675
		{
6676
			$line = trim($line);
6677
			if (empty($line) || strlen($line) != strspn($line, 'abcdefghijklmnopqrstuvwxyz0123456789-'))
6678
				return false;
6679
			else
6680
				return true;
6681
		});
6682
6683
		// Convert Punycode to Unicode
6684
		require_once($sourcedir . '/Class-Punycode.php');
6685
		$Punycode = new Punycode();
6686
		$tlds = array_map(function($input) use ($Punycode)
6687
		{
6688
			return $Punycode->decode($input);
6689
		}, $tlds);
6690
	}
6691
	// Otherwise, use the 2012 list of gTLDs and ccTLDs for now and schedule a background update
6692
	else
6693
	{
6694
		$tlds = array('com', 'net', 'org', 'edu', 'gov', 'mil', 'aero', 'asia', 'biz',
6695
			'cat', 'coop', 'info', 'int', 'jobs', 'mobi', 'museum', 'name', 'post',
6696
			'pro', 'tel', 'travel', 'xxx', 'ac', 'ad', 'ae', 'af', 'ag', 'ai', 'al',
6697
			'am', 'ao', 'aq', 'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd',
6698
			'be', 'bf', 'bg', 'bh', 'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv',
6699
			'bw', 'by', 'bz', 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm',
6700
			'cn', 'co', 'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do',
6701
			'dz', 'ec', 'ee', 'eg', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo',
6702
			'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp',
6703
			'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu',
6704
			'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm', 'jo',
6705
			'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la',
6706
			'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md',
6707
			'me', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp', 'mq', 'mr', 'ms', 'mt',
6708
			'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf', 'ng', 'ni', 'nl',
6709
			'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl',
6710
			'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw',
6711
			'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn',
6712
			'so', 'sr', 'ss', 'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg',
6713
			'th', 'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua',
6714
			'ug', 'uk', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf',
6715
			'ws', 'ye', 'yt', 'za', 'zm', 'zw',
6716
		);
6717
6718
		// Schedule a background update, unless civilization has collapsed and/or we are having connectivity issues.
6719
		if (empty($postapocalypticNightmare))
6720
		{
6721
			$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
6722
				array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
6723
				array('$sourcedir/tasks/UpdateTldRegex.php', 'Update_TLD_Regex', '', 0), array()
6724
			);
6725
		}
6726
	}
6727
6728
	// Tack on some "special use domain names" that aren't in DNS but may possibly resolve.
6729
	// See https://www.iana.org/assignments/special-use-domain-names/ for more info.
6730
	$tlds = array_merge($tlds, array('local', 'onion', 'test'));
6731
6732
	// Get an optimized regex to match all the TLDs
6733
	$tld_regex = build_regex($tlds);
6734
6735
	// Remember the new regex in $modSettings
6736
	updateSettings(array('tld_regex' => $tld_regex));
6737
6738
	// Redundant repetition is redundant
6739
	$done = true;
6740
}
6741
6742
/**
6743
 * Creates optimized regular expressions from an array of strings.
6744
 *
6745
 * An optimized regex built using this function will be much faster than a
6746
 * simple regex built using `implode('|', $strings)` --- anywhere from several
6747
 * times to several orders of magnitude faster.
6748
 *
6749
 * However, the time required to build the optimized regex is approximately
6750
 * equal to the time it takes to execute the simple regex. Therefore, it is only
6751
 * worth calling this function if the resulting regex will be used more than
6752
 * once.
6753
 *
6754
 * Because PHP places an upper limit on the allowed length of a regex, very
6755
 * large arrays of $strings may not fit in a single regex. Normally, the excess
6756
 * strings will simply be dropped. However, if the $returnArray parameter is set
6757
 * to true, this function will build as many regexes as necessary to accommodate
6758
 * everything in $strings and return them in an array. You will need to iterate
6759
 * through all elements of the returned array in order to test all possible
6760
 * matches.
6761
 *
6762
 * @param array $strings An array of strings to make a regex for.
6763
 * @param string $delim An optional delimiter character to pass to preg_quote().
6764
 * @param bool $returnArray If true, returns an array of regexes.
6765
 * @return string|array One or more regular expressions to match any of the input strings.
6766
 */
6767
function build_regex($strings, $delim = null, $returnArray = false)
6768
{
6769
	global $smcFunc;
6770
	static $regexes = array();
6771
6772
	// If it's not an array, there's not much to do. ;)
6773
	if (!is_array($strings))
0 ignored issues
show
introduced by
The condition is_array($strings) is always true.
Loading history...
6774
		return preg_quote(@strval($strings), $delim);
6775
6776
	$regex_key = md5(json_encode(array($strings, $delim, $returnArray)));
6777
6778
	if (isset($regexes[$regex_key]))
6779
		return $regexes[$regex_key];
6780
6781
	// The mb_* functions are faster than the $smcFunc ones, but may not be available
6782
	if (function_exists('mb_internal_encoding') && function_exists('mb_detect_encoding') && function_exists('mb_strlen') && function_exists('mb_substr'))
6783
	{
6784
		if (($string_encoding = mb_detect_encoding(implode(' ', $strings))) !== false)
6785
		{
6786
			$current_encoding = mb_internal_encoding();
6787
			mb_internal_encoding($string_encoding);
6788
		}
6789
6790
		$strlen = 'mb_strlen';
6791
		$substr = 'mb_substr';
6792
	}
6793
	else
6794
	{
6795
		$strlen = $smcFunc['strlen'];
6796
		$substr = $smcFunc['substr'];
6797
	}
6798
6799
	// This recursive function creates the index array from the strings
6800
	$add_string_to_index = function($string, $index) use (&$strlen, &$substr, &$add_string_to_index)
6801
	{
6802
		static $depth = 0;
6803
		$depth++;
6804
6805
		$first = (string) @$substr($string, 0, 1);
6806
6807
		// No first character? That's no good.
6808
		if ($first === '')
6809
		{
6810
			// A nested array? Really? Ugh. Fine.
6811
			if (is_array($string) && $depth < 20)
6812
			{
6813
				foreach ($string as $str)
6814
					$index = $add_string_to_index($str, $index);
6815
			}
6816
6817
			$depth--;
6818
			return $index;
6819
		}
6820
6821
		if (empty($index[$first]))
6822
			$index[$first] = array();
6823
6824
		if ($strlen($string) > 1)
6825
		{
6826
			// Sanity check on recursion
6827
			if ($depth > 99)
6828
				$index[$first][$substr($string, 1)] = '';
6829
6830
			else
6831
				$index[$first] = $add_string_to_index($substr($string, 1), $index[$first]);
6832
		}
6833
		else
6834
			$index[$first][''] = '';
6835
6836
		$depth--;
6837
		return $index;
6838
	};
6839
6840
	// This recursive function turns the index array into a regular expression
6841
	$index_to_regex = function(&$index, $delim) use (&$strlen, &$index_to_regex)
6842
	{
6843
		static $depth = 0;
6844
		$depth++;
6845
6846
		// Absolute max length for a regex is 32768, but we might need wiggle room
6847
		$max_length = 30000;
6848
6849
		$regex = array();
6850
		$length = 0;
6851
6852
		foreach ($index as $key => $value)
6853
		{
6854
			$key_regex = preg_quote($key, $delim);
6855
			$new_key = $key;
6856
6857
			if (empty($value))
6858
				$sub_regex = '';
6859
			else
6860
			{
6861
				$sub_regex = $index_to_regex($value, $delim);
6862
6863
				if (count(array_keys($value)) == 1)
6864
				{
6865
					$new_key_array = explode('(?' . '>', $sub_regex);
6866
					$new_key .= $new_key_array[0];
6867
				}
6868
				else
6869
					$sub_regex = '(?' . '>' . $sub_regex . ')';
6870
			}
6871
6872
			if ($depth > 1)
6873
				$regex[$new_key] = $key_regex . $sub_regex;
6874
			else
6875
			{
6876
				if (($length += strlen($key_regex) + 1) < $max_length || empty($regex))
6877
				{
6878
					$regex[$new_key] = $key_regex . $sub_regex;
6879
					unset($index[$key]);
6880
				}
6881
				else
6882
					break;
6883
			}
6884
		}
6885
6886
		// Sort by key length and then alphabetically
6887
		uksort($regex, function($k1, $k2) use (&$strlen)
6888
		{
6889
			$l1 = $strlen($k1);
6890
			$l2 = $strlen($k2);
6891
6892
			if ($l1 == $l2)
6893
				return strcmp($k1, $k2) > 0 ? 1 : -1;
6894
			else
6895
				return $l1 > $l2 ? -1 : 1;
6896
		});
6897
6898
		$depth--;
6899
		return implode('|', $regex);
6900
	};
6901
6902
	// Now that the functions are defined, let's do this thing
6903
	$index = array();
6904
	$regex = '';
6905
6906
	foreach ($strings as $string)
6907
		$index = $add_string_to_index($string, $index);
6908
6909
	if ($returnArray === true)
6910
	{
6911
		$regex = array();
6912
		while (!empty($index))
6913
			$regex[] = '(?' . '>' . $index_to_regex($index, $delim) . ')';
6914
	}
6915
	else
6916
		$regex = '(?' . '>' . $index_to_regex($index, $delim) . ')';
6917
6918
	// Restore PHP's internal character encoding to whatever it was originally
6919
	if (!empty($current_encoding))
6920
		mb_internal_encoding($current_encoding);
6921
6922
	$regexes[$regex_key] = $regex;
6923
	return $regex;
6924
}
6925
6926
/**
6927
 * Check if the passed url has an SSL certificate.
6928
 *
6929
 * Returns true if a cert was found & false if not.
6930
 *
6931
 * @param string $url to check, in $boardurl format (no trailing slash).
6932
 */
6933
function ssl_cert_found($url)
6934
{
6935
	// This check won't work without OpenSSL
6936
	if (!extension_loaded('openssl'))
6937
		return true;
6938
6939
	// First, strip the subfolder from the passed url, if any
6940
	$parsedurl = parse_url($url);
6941
	$url = 'ssl://' . $parsedurl['host'] . ':443';
6942
6943
	// Next, check the ssl stream context for certificate info
6944
	if (version_compare(PHP_VERSION, '5.6.0', '<'))
6945
		$ssloptions = array("capture_peer_cert" => true);
6946
	else
6947
		$ssloptions = array("capture_peer_cert" => true, "verify_peer" => true, "allow_self_signed" => true);
6948
6949
	$result = false;
6950
	$context = stream_context_create(array("ssl" => $ssloptions));
6951
	$stream = @stream_socket_client($url, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
6952
	if ($stream !== false)
6953
	{
6954
		$params = stream_context_get_params($stream);
6955
		$result = isset($params["options"]["ssl"]["peer_certificate"]) ? true : false;
6956
	}
6957
	return $result;
6958
}
6959
6960
/**
6961
 * Check if the passed url has a redirect to https:// by querying headers.
6962
 *
6963
 * Returns true if a redirect was found & false if not.
6964
 * Note that when force_ssl = 2, SMF issues its own redirect...  So if this
6965
 * returns true, it may be caused by SMF, not necessarily an .htaccess redirect.
6966
 *
6967
 * @param string $url to check, in $boardurl format (no trailing slash).
6968
 */
6969
function https_redirect_active($url)
6970
{
6971
	// Ask for the headers for the passed url, but via http...
6972
	// Need to add the trailing slash, or it puts it there & thinks there's a redirect when there isn't...
6973
	$url = str_ireplace('https://', 'http://', $url) . '/';
6974
	$headers = @get_headers($url);
6975
	if ($headers === false)
6976
		return false;
6977
6978
	// Now to see if it came back https...
6979
	// First check for a redirect status code in first row (301, 302, 307)
6980
	if (strstr($headers[0], '301') === false && strstr($headers[0], '302') === false && strstr($headers[0], '307') === false)
6981
		return false;
6982
6983
	// Search for the location entry to confirm https
6984
	$result = false;
6985
	foreach ($headers as $header)
6986
	{
6987
		if (stristr($header, 'Location: https://') !== false)
6988
		{
6989
			$result = true;
6990
			break;
6991
		}
6992
	}
6993
	return $result;
6994
}
6995
6996
/**
6997
 * Build query_wanna_see_board and query_see_board for a userid
6998
 *
6999
 * Returns array with keys query_wanna_see_board and query_see_board
7000
 *
7001
 * @param int $userid of the user
7002
 */
7003
function build_query_board($userid)
7004
{
7005
	global $user_info, $modSettings, $smcFunc, $db_prefix;
7006
7007
	$query_part = array();
7008
7009
	// If we come from cron, we can't have a $user_info.
7010
	if (isset($user_info['id']) && $user_info['id'] == $userid && SMF != 'BACKGROUND')
7011
	{
7012
		$groups = $user_info['groups'];
7013
		$can_see_all_boards = $user_info['is_admin'] || $user_info['can_manage_boards'];
7014
		$ignoreboards = !empty($user_info['ignoreboards']) ? $user_info['ignoreboards'] : null;
7015
	}
7016
	else
7017
	{
7018
		$request = $smcFunc['db_query']('', '
7019
			SELECT mem.ignore_boards, mem.id_group, mem.additional_groups, mem.id_post_group
7020
			FROM {db_prefix}members AS mem
7021
			WHERE mem.id_member = {int:id_member}
7022
			LIMIT 1',
7023
			array(
7024
				'id_member' => $userid,
7025
			)
7026
		);
7027
7028
		$row = $smcFunc['db_fetch_assoc']($request);
7029
7030
		if (empty($row['additional_groups']))
7031
			$groups = array($row['id_group'], $row['id_post_group']);
7032
		else
7033
			$groups = array_merge(
7034
				array($row['id_group'], $row['id_post_group']),
7035
				explode(',', $row['additional_groups'])
7036
			);
7037
7038
		// Because history has proven that it is possible for groups to go bad - clean up in case.
7039
		foreach ($groups as $k => $v)
7040
			$groups[$k] = (int) $v;
7041
7042
		$can_see_all_boards = in_array(1, $groups) || (!empty($modSettings['board_manager_groups']) && count(array_intersect($groups, explode(',', $modSettings['board_manager_groups']))) > 0);
7043
7044
		$ignoreboards = !empty($row['ignore_boards']) && !empty($modSettings['allow_ignore_boards']) ? explode(',', $row['ignore_boards']) : array();
7045
	}
7046
7047
	// Just build this here, it makes it easier to change/use - administrators can see all boards.
7048
	if ($can_see_all_boards)
7049
		$query_part['query_see_board'] = '1=1';
7050
	// Otherwise just the groups in $user_info['groups'].
7051
	else
7052
	{
7053
		$query_part['query_see_board'] = '
7054
			EXISTS (
7055
				SELECT bpv.id_board
7056
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
7057
				WHERE bpv.id_group IN ('. implode(',', $groups) .')
7058
					AND bpv.deny = 0
7059
					AND bpv.id_board = b.id_board
7060
			)';
7061
7062
		if (!empty($modSettings['deny_boards_access']))
7063
			$query_part['query_see_board'] .= '
7064
			AND NOT EXISTS (
7065
				SELECT bpv.id_board
7066
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
7067
				WHERE bpv.id_group IN ( '. implode(',', $groups) .')
7068
					AND bpv.deny = 1
7069
					AND bpv.id_board = b.id_board
7070
			)';
7071
	}
7072
7073
	$query_part['query_see_message_board'] = str_replace('b.', 'm.', $query_part['query_see_board']);
7074
	$query_part['query_see_topic_board'] = str_replace('b.', 't.', $query_part['query_see_board']);
7075
7076
	// Build the list of boards they WANT to see.
7077
	// This will take the place of query_see_boards in certain spots, so it better include the boards they can see also
7078
7079
	// If they aren't ignoring any boards then they want to see all the boards they can see
7080
	if (empty($ignoreboards))
7081
	{
7082
		$query_part['query_wanna_see_board'] = $query_part['query_see_board'];
7083
		$query_part['query_wanna_see_message_board'] = $query_part['query_see_message_board'];
7084
		$query_part['query_wanna_see_topic_board'] = $query_part['query_see_topic_board'];
7085
	}
7086
	// Ok I guess they don't want to see all the boards
7087
	else
7088
	{
7089
		$query_part['query_wanna_see_board'] = '(' . $query_part['query_see_board'] . ' AND b.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7090
		$query_part['query_wanna_see_message_board'] = '(' . $query_part['query_see_message_board'] . ' AND m.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7091
		$query_part['query_wanna_see_topic_board'] = '(' . $query_part['query_see_topic_board'] . ' AND t.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7092
	}
7093
7094
	return $query_part;
7095
}
7096
7097
/**
7098
 * Check if the connection is using https.
7099
 *
7100
 * @return boolean true if connection used https
7101
 */
7102
function httpsOn()
7103
{
7104
	$secure = false;
7105
7106
	if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on')
7107
		$secure = true;
7108
	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...
7109
		$secure = true;
7110
7111
	return $secure;
7112
}
7113
7114
/**
7115
 * A wrapper for `filter_var($url, FILTER_VALIDATE_URL)` that can handle URLs
7116
 * with international characters (a.k.a. IRIs)
7117
 *
7118
 * @param string $iri The IRI to test.
7119
 * @param int $flags Optional flags to pass to filter_var()
7120
 * @return string|bool Either the original IRI, or false if the IRI was invalid.
7121
 */
7122
function validate_iri($iri, $flags = null)
7123
{
7124
	$url = iri_to_url($iri);
7125
7126
	if (filter_var($url, FILTER_VALIDATE_URL, $flags) !== false)
7127
		return $iri;
7128
	else
7129
		return false;
7130
}
7131
7132
/**
7133
 * A wrapper for `filter_var($url, FILTER_SANITIZE_URL)` that can handle URLs
7134
 * with international characters (a.k.a. IRIs)
7135
 *
7136
 * Note: The returned value will still be an IRI, not a URL. To convert to URL,
7137
 * feed the result of this function to iri_to_url()
7138
 *
7139
 * @param string $iri The IRI to sanitize.
7140
 * @return string|bool The sanitized version of the IRI
7141
 */
7142
function sanitize_iri($iri)
7143
{
7144
	// Encode any non-ASCII characters (but not space or control characters of any sort)
7145
	$iri = preg_replace_callback('~[^\x00-\x7F\pZ\pC]~u', function($matches)
7146
	{
7147
		return rawurlencode($matches[0]);
7148
	}, $iri);
7149
7150
	// Perform normal sanitization
7151
	$iri = filter_var($iri, FILTER_SANITIZE_URL);
7152
7153
	// Decode the non-ASCII characters
7154
	$iri = rawurldecode($iri);
7155
7156
	return $iri;
7157
}
7158
7159
/**
7160
 * Converts a URL with international characters (an IRI) into a pure ASCII URL
7161
 *
7162
 * Uses Punycode to encode any non-ASCII characters in the domain name, and uses
7163
 * standard URL encoding on the rest.
7164
 *
7165
 * @param string $iri A IRI that may or may not contain non-ASCII characters.
7166
 * @return string|bool The URL version of the IRI.
7167
 */
7168
function iri_to_url($iri)
7169
{
7170
	global $sourcedir;
7171
7172
	$host = parse_url((strpos($iri, '://') === false ? 'http://' : '') . ltrim($iri, ':/'), PHP_URL_HOST);
7173
7174
	if (empty($host))
7175
		return $iri;
7176
7177
	// Convert the domain using the Punycode algorithm
7178
	require_once($sourcedir . '/Class-Punycode.php');
7179
	$Punycode = new Punycode();
7180
	$encoded_host = $Punycode->encode($host);
7181
	$pos = strpos($iri, $host);
7182
	$iri = substr_replace($iri, $encoded_host, $pos, strlen($host));
7183
7184
	// Encode any disallowed characters in the rest of the URL
7185
	$unescaped = array(
7186
		'%21' => '!', '%23' => '#', '%24' => '$', '%26' => '&',
7187
		'%27' => "'", '%28' => '(', '%29' => ')', '%2A' => '*',
7188
		'%2B' => '+', '%2C' => ',', '%2F' => '/', '%3A' => ':',
7189
		'%3B' => ';', '%3D' => '=', '%3F' => '?', '%40' => '@',
7190
		'%25' => '%',
7191
	);
7192
	$iri = strtr(rawurlencode($iri), $unescaped);
7193
7194
	return $iri;
7195
}
7196
7197
/**
7198
 * Decodes a URL containing encoded international characters to UTF-8
7199
 *
7200
 * Decodes any Punycode encoded characters in the domain name, then uses
7201
 * standard URL decoding on the rest.
7202
 *
7203
 * @param string $url The pure ASCII version of a URL.
7204
 * @return string|bool The UTF-8 version of the URL.
7205
 */
7206
function url_to_iri($url)
7207
{
7208
	global $sourcedir;
7209
7210
	$host = parse_url((strpos($url, '://') === false ? 'http://' : '') . ltrim($url, ':/'), PHP_URL_HOST);
7211
7212
	if (empty($host))
7213
		return $url;
7214
7215
	// Decode the domain from Punycode
7216
	require_once($sourcedir . '/Class-Punycode.php');
7217
	$Punycode = new Punycode();
7218
	$decoded_host = $Punycode->decode($host);
7219
	$pos = strpos($url, $host);
7220
	$url = substr_replace($url, $decoded_host, $pos, strlen($host));
7221
7222
	// Decode the rest of the URL
7223
	$url = rawurldecode($url);
7224
7225
	return $url;
7226
}
7227
7228
/**
7229
 * Ensures SMF's scheduled tasks are being run as intended
7230
 *
7231
 * If the admin activated the cron_is_real_cron setting, but the cron job is
7232
 * not running things at least once per day, we need to go back to SMF's default
7233
 * behaviour using "web cron" JavaScript calls.
7234
 */
7235
function check_cron()
7236
{
7237
	global $modSettings, $smcFunc, $txt;
7238
7239
	if (!empty($modSettings['cron_is_real_cron']) && time() - @intval($modSettings['cron_last_checked']) > 84600)
7240
	{
7241
		$request = $smcFunc['db_query']('', '
7242
			SELECT COUNT(*)
7243
			FROM {db_prefix}scheduled_tasks
7244
			WHERE disabled = {int:not_disabled}
7245
				AND next_time < {int:yesterday}',
7246
			array(
7247
				'not_disabled' => 0,
7248
				'yesterday' => time() - 84600,
7249
			)
7250
		);
7251
		list($overdue) = $smcFunc['db_fetch_row']($request);
7252
		$smcFunc['db_free_result']($request);
7253
7254
		// If we have tasks more than a day overdue, cron isn't doing its job.
7255
		if (!empty($overdue))
7256
		{
7257
			loadLanguage('ManageScheduledTasks');
7258
			log_error($txt['cron_not_working']);
7259
			updateSettings(array('cron_is_real_cron' => 0));
7260
		}
7261
		else
7262
			updateSettings(array('cron_last_checked' => time()));
7263
	}
7264
}
7265
7266
/**
7267
 * Sends an appropriate HTTP status header based on a given status code
7268
 *
7269
 * @param int $code The status code
7270
 * @param string $status The string for the status. Set automatically if not provided.
7271
 */
7272
function send_http_status($code, $status = '')
7273
{
7274
	$statuses = array(
7275
		206 => 'Partial Content',
7276
		304 => 'Not Modified',
7277
		400 => 'Bad Request',
7278
		403 => 'Forbidden',
7279
		404 => 'Not Found',
7280
		410 => 'Gone',
7281
		500 => 'Internal Server Error',
7282
		503 => 'Service Unavailable',
7283
	);
7284
7285
	$protocol = preg_match('~^\s*(HTTP/[12]\.\d)\s*$~i', $_SERVER['SERVER_PROTOCOL'], $matches) ? $matches[1] : 'HTTP/1.0';
7286
7287
	if (!isset($statuses[$code]) && empty($status))
7288
		header($protocol . ' 500 Internal Server Error');
7289
	else
7290
		header($protocol . ' ' . $code . ' ' . (!empty($status) ? $status : $statuses[$code]));
7291
}
7292
7293
/**
7294
 * Concatenates an array of strings into a grammatically correct sentence list
7295
 *
7296
 * Uses formats defined in the language files to build the list appropropriately
7297
 * for the currently loaded language.
7298
 *
7299
 * @param array $list An array of strings to concatenate.
7300
 * @return string The localized sentence list.
7301
 */
7302
function sentence_list($list)
7303
{
7304
	global $txt;
7305
7306
	// Make sure the bare necessities are defined
7307
	if (empty($txt['sentence_list_format']['n']))
7308
		$txt['sentence_list_format']['n'] = '{series}';
7309
	if (!isset($txt['sentence_list_separator']))
7310
		$txt['sentence_list_separator'] = ', ';
7311
	if (!isset($txt['sentence_list_separator_alt']))
7312
		$txt['sentence_list_separator_alt'] = '; ';
7313
7314
	// Which format should we use?
7315
	if (isset($txt['sentence_list_format'][count($list)]))
7316
		$format = $txt['sentence_list_format'][count($list)];
7317
	else
7318
		$format = $txt['sentence_list_format']['n'];
7319
7320
	// Do we want the normal separator or the alternate?
7321
	$separator = $txt['sentence_list_separator'];
7322
	foreach ($list as $item)
7323
	{
7324
		if (strpos($item, $separator) !== false)
7325
		{
7326
			$separator = $txt['sentence_list_separator_alt'];
7327
			$format = strtr($format, trim($txt['sentence_list_separator']), trim($separator));
7328
			break;
7329
		}
7330
	}
7331
7332
	$replacements = array();
7333
7334
	// Special handling for the last items on the list
7335
	$i = 0;
7336
	while (empty($done))
7337
	{
7338
		if (strpos($format, '{'. --$i . '}') !== false)
7339
			$replacements['{'. $i . '}'] = array_pop($list);
7340
		else
7341
			$done = true;
7342
	}
7343
	unset($done);
7344
7345
	// Special handling for the first items on the list
7346
	$i = 0;
7347
	while (empty($done))
7348
	{
7349
		if (strpos($format, '{'. ++$i . '}') !== false)
7350
			$replacements['{'. $i . '}'] = array_shift($list);
7351
		else
7352
			$done = true;
7353
	}
7354
	unset($done);
7355
7356
	// Whatever is left
7357
	$replacements['{series}'] = implode($separator, $list);
7358
7359
	// Do the deed
7360
	return strtr($format, $replacements);
7361
}
7362
7363
/**
7364
 * Truncate an array to a specified length
7365
 *
7366
 * @param array $array The array to truncate
7367
 * @param int $max_length The upperbound on the length
7368
 * @param int $deep How levels in an multidimensional array should the function take into account.
7369
 * @return array The truncated array
7370
 */
7371
function truncate_array($array, $max_length = 1900, $deep = 3)
7372
{
7373
	$array = (array) $array;
7374
7375
	$curr_length = array_length($array, $deep);
7376
7377
	if ($curr_length <= $max_length)
7378
		return $array;
7379
7380
	else
7381
	{
7382
		// Truncate each element's value to a reasonable length
7383
		$param_max = floor($max_length / count($array));
7384
7385
		$current_deep = $deep - 1;
7386
7387
		foreach ($array as $key => &$value)
7388
		{
7389
			if (is_array($value))
7390
				if ($current_deep > 0)
7391
					$value = truncate_array($value, $current_deep);
7392
7393
			else
7394
				$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 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

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

7394
				$value = substr(/** @scrutinizer ignore-type */ $value, 0, $param_max - strlen($key) - 5);
Loading history...
7395
		}
7396
7397
		return $array;
7398
	}
7399
}
7400
7401
/**
7402
 * array_length Recursive
7403
 * @param array $array
7404
 * @param int $deep How many levels should the function
7405
 * @return int
7406
 */
7407
function array_length($array, $deep = 3)
7408
{
7409
	// Work with arrays
7410
	$array = (array) $array;
7411
	$length = 0;
7412
7413
	$deep_count = $deep - 1;
7414
7415
	foreach ($array as $value)
7416
	{
7417
		// Recursive?
7418
		if (is_array($value))
7419
		{
7420
			// No can't do
7421
			if ($deep_count <= 0)
7422
				continue;
7423
7424
			$length += array_length($value, $deep_count);
7425
		}
7426
		else
7427
			$length += strlen($value);
7428
	}
7429
7430
	return $length;
7431
}
7432
7433
/**
7434
 * Compares existance request variables against an array.
7435
 *
7436
 * The input array is associative, where keys denote accepted values
7437
 * in a request variable denoted by `$req_val`. Values can be:
7438
 *
7439
 * - another associative array where at least one key must be found
7440
 *   in the request and their values are accepted request values.
7441
 * - A scalar value, in which case no furthur checks are done.
7442
 *
7443
 * @param array $array
7444
 * @param string $req_var request variable
7445
 *
7446
 * @return bool whether any of the criteria was satisfied
7447
 */
7448
function is_filtered_request(array $array, $req_var)
7449
{
7450
	$matched = false;
7451
	if (isset($_REQUEST[$req_var], $array[$_REQUEST[$req_var]]))
7452
	{
7453
		if (is_array($array[$_REQUEST[$req_var]]))
7454
		{
7455
			foreach ($array[$_REQUEST[$req_var]] as $subtype => $subnames)
7456
				$matched |= isset($_REQUEST[$subtype]) && in_array($_REQUEST[$subtype], $subnames);
7457
		}
7458
		else
7459
			$matched = true;
7460
	}
7461
7462
	return (bool) $matched;
7463
}
7464
7465
/**
7466
 * Clean up the XML to make sure it doesn't contain invalid characters.
7467
 *
7468
 * See https://www.w3.org/TR/xml/#charsets
7469
 *
7470
 * @param string $string The string to clean
7471
 * @return string The cleaned string
7472
 */
7473
function cleanXml($string)
7474
{
7475
	global $context;
7476
7477
	$illegal_chars = array(
7478
		// Remove all ASCII control characters except \t, \n, and \r.
7479
		"\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08",
7480
		"\x0B", "\x0C", "\x0E", "\x0F", "\x10", "\x11", "\x12", "\x13", "\x14",
7481
		"\x15", "\x16", "\x17", "\x18", "\x19", "\x1A", "\x1B", "\x1C", "\x1D",
7482
		"\x1E", "\x1F",
7483
		// Remove \xFFFE and \xFFFF
7484
		"\xEF\xBF\xBE", "\xEF\xBF\xBF",
7485
	);
7486
7487
	$string = str_replace($illegal_chars, '', $string);
7488
7489
	// The Unicode surrogate pair code points should never be present in our
7490
	// strings to begin with, but if any snuck in, they need to be removed.
7491
	if (!empty($context['utf8']) && strpos($string, "\xED") !== false)
7492
		$string = preg_replace('/\xED[\xA0-\xBF][\x80-\xBF]/', '', $string);
7493
7494
	return $string;
7495
}
7496
7497
/**
7498
 * Escapes (replaces) characters in strings to make them safe for use in javascript
7499
 *
7500
 * @param string $string The string to escape
7501
 * @return string The escaped string
7502
 */
7503
function JavaScriptEscape($string)
7504
{
7505
	global $scripturl;
7506
7507
	return '\'' . strtr($string, array(
7508
		"\r" => '',
7509
		"\n" => '\\n',
7510
		"\t" => '\\t',
7511
		'\\' => '\\\\',
7512
		'\'' => '\\\'',
7513
		'</' => '<\' + \'/',
7514
		'<script' => '<scri\'+\'pt',
7515
		'<body>' => '<bo\'+\'dy>',
7516
		'<a href' => '<a hr\'+\'ef',
7517
		$scripturl => '\' + smf_scripturl + \'',
7518
	)) . '\'';
7519
}
7520
7521
?>