Passed
Pull Request — release-2.1 (#6262)
by Jeremy
04:04
created

smf_list_timezones()   D

Complexity

Conditions 26

Size

Total Lines 131
Code Lines 67

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 1 Features 0
Metric Value
cc 26
eloc 67
c 4
b 1
f 0
nop 1
dl 0
loc 131
rs 4.1666

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

1545
						for ($php_i = 0, $php_n = count(/** @scrutinizer ignore-type */ $php_parts); $php_i < $php_n; $php_i++)
Loading history...
1546
						{
1547
							// Do PHP code coloring?
1548
							if ($php_parts[$php_i] != '&lt;?php')
1549
								continue;
1550
1551
							$php_string = '';
1552
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1553
							{
1554
								$php_string .= $php_parts[$php_i];
1555
								$php_parts[$php_i++] = '';
1556
							}
1557
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1558
						}
1559
1560
						// Fix the PHP code stuff...
1561
						$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

1561
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', /** @scrutinizer ignore-type */ $php_parts));
Loading history...
1562
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1563
1564
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1565
						if (!empty($context['browser']['is_opera']))
1566
							$data .= '&nbsp;';
1567
					}
1568
				},
1569
				'block_level' => true,
1570
			),
1571
			array(
1572
				'tag' => 'code',
1573
				'type' => 'unparsed_equals_content',
1574
				'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>',
1575
				// @todo Maybe this can be simplified?
1576
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1577
				{
1578
					if (!isset($disabled['code']))
1579
					{
1580
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1581
1582
						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

1582
						for ($php_i = 0, $php_n = count(/** @scrutinizer ignore-type */ $php_parts); $php_i < $php_n; $php_i++)
Loading history...
1583
						{
1584
							// Do PHP code coloring?
1585
							if ($php_parts[$php_i] != '&lt;?php')
1586
								continue;
1587
1588
							$php_string = '';
1589
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1590
							{
1591
								$php_string .= $php_parts[$php_i];
1592
								$php_parts[$php_i++] = '';
1593
							}
1594
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1595
						}
1596
1597
						// Fix the PHP code stuff...
1598
						$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

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

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

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

2769
				while ($blob_counter <= count(/** @scrutinizer ignore-type */ $blobs))
Loading history...
2770
				{
2771
					$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

2771
					$given_param_string = implode(']', array_slice(/** @scrutinizer ignore-type */ $blobs, 0, $blob_counter++));
Loading history...
2772
2773
					$given_params = preg_split('~\s(?=(' . $splitters . '))~i', $given_param_string);
2774
					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

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

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

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

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

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

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

5616
			$temp_file = tempnam($cachedir, md5(/** @scrutinizer ignore-type */ $data));
Loading history...
5617
			file_put_contents($temp_file, $data);
5618
			$is_path = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $is_path is dead and can be removed.
Loading history...
5619
			$data = $temp_file;
5620
		}
5621
5622
		$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

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

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

5773
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5774
5775
	// remove left to right / right to left overrides
5776
	if ($num === 0x202D || $num === 0x202E)
5777
		return '';
5778
5779
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
5780
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
5781
		return '&#' . $num . ';';
5782
5783
	if (empty($context['utf8']))
5784
	{
5785
		// no control characters
5786
		if ($num < 0x20)
5787
			return '';
5788
		// text is text
5789
		elseif ($num < 0x80)
5790
			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

5790
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5791
		// all others get html-ised
5792
		else
5793
			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

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

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

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

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

5964
	$tzids = array_merge(array_keys($tztxt), array_diff(timezone_identifiers_list(), array_keys($tztxt), $low_priority_tzids), /** @scrutinizer ignore-type */ $low_priority_tzids);
Loading history...
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

5964
	$tzids = array_merge(array_keys($tztxt), array_diff(timezone_identifiers_list(), array_keys($tztxt), /** @scrutinizer ignore-type */ $low_priority_tzids), $low_priority_tzids);
Loading history...
Bug introduced by
It seems like timezone_identifiers_list() can also be of type false; however, parameter $array1 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

5964
	$tzids = array_merge(array_keys($tztxt), array_diff(/** @scrutinizer ignore-type */ timezone_identifiers_list(), array_keys($tztxt), $low_priority_tzids), $low_priority_tzids);
Loading history...
5965
5966
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
5967
	foreach ($tzids as $tzid)
5968
	{
5969
		// We don't want UTC right now
5970
		if ($tzid == 'UTC')
5971
			continue;
5972
5973
		$tz = timezone_open($tzid);
5974
5975
		// First, get the set of transition rules for this tzid
5976
		$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

5976
		$tzinfo = timezone_transitions_get(/** @scrutinizer ignore-type */ $tz, $when, $later);
Loading history...
5977
5978
		// Use the entire set of transition rules as the array *key* so we can avoid duplicates
5979
		$tzkey = serialize($tzinfo);
5980
5981
		// Next, get the geographic info for this tzid
5982
		$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

5982
		$tzgeo = timezone_location_get(/** @scrutinizer ignore-type */ $tz);
Loading history...
5983
5984
		// Don't overwrite our preferred tzids
5985
		if (empty($zones[$tzkey]['tzid']))
5986
		{
5987
			$zones[$tzkey]['tzid'] = $tzid;
5988
			$zones[$tzkey]['abbr'] = $tzinfo[0]['abbr'];
5989
		}
5990
5991
		// A time zone from a prioritized country?
5992
		if (in_array($tzid, $priority_tzids))
5993
			$priority_zones[$tzkey] = true;
5994
5995
		// Keep track of the location and offset for this tzid
5996
		if (!empty($txt[$tzid]))
5997
			$zones[$tzkey]['locations'][] = $txt[$tzid];
5998
		else
5999
		{
6000
			$tzid_parts = explode('/', $tzid);
6001
			$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
6002
		}
6003
		$offsets[$tzkey] = $tzinfo[0]['offset'];
6004
		$longitudes[$tzkey] = empty($longitudes[$tzkey]) ? $tzgeo['longitude'] : $longitudes[$tzkey];
6005
6006
		// Remember this for later
6007
		if (isset($cur_profile['timezone']) && $cur_profile['timezone'] == $tzid)
6008
			$member_tzkey = $tzkey;
6009
	}
6010
6011
	// Sort by offset then longitude
6012
	array_multisort($offsets, SORT_ASC, SORT_NUMERIC, $longitudes, SORT_ASC, SORT_NUMERIC, $zones);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $longitudes seems to be defined by a foreach iteration on line 5967. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
Comprehensibility Best Practice introduced by
The variable $zones seems to be defined by a foreach iteration on line 5967. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
6013
6014
	// Build the final array of formatted values
6015
	$priority_timezones = array();
6016
	$timezones = array();
6017
	foreach ($zones as $tzkey => $tzvalue)
6018
	{
6019
		date_timezone_set($date_when, timezone_open($tzvalue['tzid']));
0 ignored issues
show
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

6019
		date_timezone_set($date_when, /** @scrutinizer ignore-type */ timezone_open($tzvalue['tzid']));
Loading history...
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

6019
		date_timezone_set(/** @scrutinizer ignore-type */ $date_when, timezone_open($tzvalue['tzid']));
Loading history...
6020
6021
		// Use the custom description, if there is one
6022
		if (!empty($tztxt[$tzvalue['tzid']]))
6023
			$desc = $tztxt[$tzvalue['tzid']];
6024
		// Otherwise, use the list of locations (max 5, so things don't get silly)
6025
		else
6026
			$desc = implode(', ', array_slice(array_unique($tzvalue['locations']), 0, 5)) . (count($tzvalue['locations']) > 5 ? ', ' . $txt['etc'] : '');
6027
6028
		// Show the UTC offset and the abbreviation, if it's something like 'MST' and not '-06'
6029
		$desc = '[UTC' . date_format($date_when, 'P') . '] - ' . (!strspn($tzvalue['abbr'], '+-') ? $tzvalue['abbr'] . ' - ' : '') . $desc;
6030
6031
		if (isset($priority_zones[$tzkey]))
6032
			$priority_timezones[$tzvalue['tzid']] = $desc;
6033
		else
6034
			$timezones[$tzvalue['tzid']] = $desc;
6035
6036
		// Automatically fix orphaned timezones on the member profile page
6037
		if (isset($member_tzkey) && $member_tzkey == $tzkey)
6038
			$cur_profile['timezone'] = $tzvalue['tzid'];
6039
	}
6040
6041
	if (!empty($priority_timezones))
6042
		$priority_timezones[] = '-----';
6043
6044
	$timezones = array_merge(
6045
		$priority_timezones,
6046
		array('UTC' => 'UTC' . (!empty($tztxt['UTC']) ? ' - ' . $tztxt['UTC'] : ''), '-----'),
6047
		$timezones
6048
	);
6049
6050
	return $timezones;
6051
}
6052
6053
/**
6054
 * Converts an IP address into binary
6055
 *
6056
 * @param string $ip_address An IP address in IPv4, IPv6 or decimal notation
6057
 * @return string|false The IP address in binary or false
6058
 */
6059
function inet_ptod($ip_address)
6060
{
6061
	if (!isValidIP($ip_address))
6062
		return $ip_address;
6063
6064
	$bin = inet_pton($ip_address);
6065
	return $bin;
6066
}
6067
6068
/**
6069
 * Converts a binary version of an IP address into a readable format
6070
 *
6071
 * @param string $bin An IP address in IPv4, IPv6 (Either string (postgresql) or binary (other databases))
6072
 * @return string|false The IP address in presentation format or false on error
6073
 */
6074
function inet_dtop($bin)
6075
{
6076
	global $db_type;
6077
6078
	if (empty($bin))
6079
		return '';
6080
	elseif ($db_type == 'postgresql')
6081
		return $bin;
6082
	// Already a String?
6083
	elseif (isValidIP($bin))
6084
		return $bin;
6085
	return inet_ntop($bin);
6086
}
6087
6088
/**
6089
 * Safe serialize() and unserialize() replacements
6090
 *
6091
 * @license Public Domain
6092
 *
6093
 * @author anthon (dot) pang (at) gmail (dot) com
6094
 */
6095
6096
/**
6097
 * Safe serialize() replacement. Recursive
6098
 * - output a strict subset of PHP's native serialized representation
6099
 * - does not serialize objects
6100
 *
6101
 * @param mixed $value
6102
 * @return string
6103
 */
6104
function _safe_serialize($value)
6105
{
6106
	if (is_null($value))
6107
		return 'N;';
6108
6109
	if (is_bool($value))
6110
		return 'b:' . (int) $value . ';';
6111
6112
	if (is_int($value))
6113
		return 'i:' . $value . ';';
6114
6115
	if (is_float($value))
6116
		return 'd:' . str_replace(',', '.', $value) . ';';
6117
6118
	if (is_string($value))
6119
		return 's:' . strlen($value) . ':"' . $value . '";';
6120
6121
	if (is_array($value))
6122
	{
6123
		$out = '';
6124
		foreach ($value as $k => $v)
6125
			$out .= _safe_serialize($k) . _safe_serialize($v);
6126
6127
		return 'a:' . count($value) . ':{' . $out . '}';
6128
	}
6129
6130
	// safe_serialize cannot serialize resources or objects.
6131
	return false;
6132
}
6133
6134
/**
6135
 * Wrapper for _safe_serialize() that handles exceptions and multibyte encoding issues.
6136
 *
6137
 * @param mixed $value
6138
 * @return string
6139
 */
6140
function safe_serialize($value)
6141
{
6142
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
6143
	if (function_exists('mb_internal_encoding') &&
6144
		(((int) ini_get('mbstring.func_overload')) & 2))
6145
	{
6146
		$mbIntEnc = mb_internal_encoding();
6147
		mb_internal_encoding('ASCII');
6148
	}
6149
6150
	$out = _safe_serialize($value);
6151
6152
	if (isset($mbIntEnc))
6153
		mb_internal_encoding($mbIntEnc);
6154
6155
	return $out;
6156
}
6157
6158
/**
6159
 * Safe unserialize() replacement
6160
 * - accepts a strict subset of PHP's native serialized representation
6161
 * - does not unserialize objects
6162
 *
6163
 * @param string $str
6164
 * @return mixed
6165
 * @throw Exception if $str is malformed or contains unsupported types (e.g., resources, objects)
6166
 */
6167
function _safe_unserialize($str)
6168
{
6169
	// Input  is not a string.
6170
	if (empty($str) || !is_string($str))
6171
		return false;
6172
6173
	$stack = array();
6174
	$expected = array();
6175
6176
	/*
6177
	 * states:
6178
	 *   0 - initial state, expecting a single value or array
6179
	 *   1 - terminal state
6180
	 *   2 - in array, expecting end of array or a key
6181
	 *   3 - in array, expecting value or another array
6182
	 */
6183
	$state = 0;
6184
	while ($state != 1)
6185
	{
6186
		$type = isset($str[0]) ? $str[0] : '';
6187
		if ($type == '}')
6188
			$str = substr($str, 1);
6189
6190
		elseif ($type == 'N' && $str[1] == ';')
6191
		{
6192
			$value = null;
6193
			$str = substr($str, 2);
6194
		}
6195
		elseif ($type == 'b' && preg_match('/^b:([01]);/', $str, $matches))
6196
		{
6197
			$value = $matches[1] == '1' ? true : false;
6198
			$str = substr($str, 4);
6199
		}
6200
		elseif ($type == 'i' && preg_match('/^i:(-?[0-9]+);(.*)/s', $str, $matches))
6201
		{
6202
			$value = (int) $matches[1];
6203
			$str = $matches[2];
6204
		}
6205
		elseif ($type == 'd' && preg_match('/^d:(-?[0-9]+\.?[0-9]*(E[+-][0-9]+)?);(.*)/s', $str, $matches))
6206
		{
6207
			$value = (float) $matches[1];
6208
			$str = $matches[3];
6209
		}
6210
		elseif ($type == 's' && preg_match('/^s:([0-9]+):"(.*)/s', $str, $matches) && substr($matches[2], (int) $matches[1], 2) == '";')
6211
		{
6212
			$value = substr($matches[2], 0, (int) $matches[1]);
6213
			$str = substr($matches[2], (int) $matches[1] + 2);
6214
		}
6215
		elseif ($type == 'a' && preg_match('/^a:([0-9]+):{(.*)/s', $str, $matches))
6216
		{
6217
			$expectedLength = (int) $matches[1];
6218
			$str = $matches[2];
6219
		}
6220
6221
		// Object or unknown/malformed type.
6222
		else
6223
			return false;
6224
6225
		switch ($state)
6226
		{
6227
			case 3: // In array, expecting value or another array.
6228
				if ($type == 'a')
6229
				{
6230
					$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...
6231
					$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...
6232
					$list = &$list[$key];
6233
					$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...
6234
					$state = 2;
6235
					break;
6236
				}
6237
				if ($type != '}')
6238
				{
6239
					$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...
6240
					$state = 2;
6241
					break;
6242
				}
6243
6244
				// Missing array value.
6245
				return false;
6246
6247
			case 2: // in array, expecting end of array or a key
6248
				if ($type == '}')
6249
				{
6250
					// Array size is less than expected.
6251
					if (count($list) < end($expected))
6252
						return false;
6253
6254
					unset($list);
6255
					$list = &$stack[count($stack) - 1];
6256
					array_pop($stack);
6257
6258
					// Go to terminal state if we're at the end of the root array.
6259
					array_pop($expected);
6260
6261
					if (count($expected) == 0)
6262
						$state = 1;
6263
6264
					break;
6265
				}
6266
6267
				if ($type == 'i' || $type == 's')
6268
				{
6269
					// Array size exceeds expected length.
6270
					if (count($list) >= end($expected))
6271
						return false;
6272
6273
					$key = $value;
6274
					$state = 3;
6275
					break;
6276
				}
6277
6278
				// Illegal array index type.
6279
				return false;
6280
6281
			// Expecting array or value.
6282
			case 0:
6283
				if ($type == 'a')
6284
				{
6285
					$data = array();
6286
					$list = &$data;
6287
					$expected[] = $expectedLength;
6288
					$state = 2;
6289
					break;
6290
				}
6291
6292
				if ($type != '}')
6293
				{
6294
					$data = $value;
6295
					$state = 1;
6296
					break;
6297
				}
6298
6299
				// Not in array.
6300
				return false;
6301
		}
6302
	}
6303
6304
	// Trailing data in input.
6305
	if (!empty($str))
6306
		return false;
6307
6308
	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...
6309
}
6310
6311
/**
6312
 * Wrapper for _safe_unserialize() that handles exceptions and multibyte encoding issue
6313
 *
6314
 * @param string $str
6315
 * @return mixed
6316
 */
6317
function safe_unserialize($str)
6318
{
6319
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
6320
	if (function_exists('mb_internal_encoding') &&
6321
		(((int) ini_get('mbstring.func_overload')) & 0x02))
6322
	{
6323
		$mbIntEnc = mb_internal_encoding();
6324
		mb_internal_encoding('ASCII');
6325
	}
6326
6327
	$out = _safe_unserialize($str);
6328
6329
	if (isset($mbIntEnc))
6330
		mb_internal_encoding($mbIntEnc);
6331
6332
	return $out;
6333
}
6334
6335
/**
6336
 * Tries different modes to make file/dirs writable. Wrapper function for chmod()
6337
 *
6338
 * @param string $file The file/dir full path.
6339
 * @param int $value Not needed, added for legacy reasons.
6340
 * @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.
6341
 */
6342
function smf_chmod($file, $value = 0)
6343
{
6344
	// No file? no checks!
6345
	if (empty($file))
6346
		return false;
6347
6348
	// Already writable?
6349
	if (is_writable($file))
6350
		return true;
6351
6352
	// Do we have a file or a dir?
6353
	$isDir = is_dir($file);
6354
	$isWritable = false;
6355
6356
	// Set different modes.
6357
	$chmodValues = $isDir ? array(0750, 0755, 0775, 0777) : array(0644, 0664, 0666);
6358
6359
	foreach ($chmodValues as $val)
6360
	{
6361
		// If it's writable, break out of the loop.
6362
		if (is_writable($file))
6363
		{
6364
			$isWritable = true;
6365
			break;
6366
		}
6367
6368
		else
6369
			@chmod($file, $val);
6370
	}
6371
6372
	return $isWritable;
6373
}
6374
6375
/**
6376
 * Wrapper function for json_decode() with error handling.
6377
 *
6378
 * @param string $json The string to decode.
6379
 * @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.
6380
 * @param bool $logIt To specify if the error will be logged if theres any.
6381
 * @return array Either an empty array or the decoded data as an array.
6382
 */
6383
function smf_json_decode($json, $returnAsArray = false, $logIt = true)
6384
{
6385
	global $txt;
6386
6387
	// Come on...
6388
	if (empty($json) || !is_string($json))
6389
		return array();
6390
6391
	$returnArray = @json_decode($json, $returnAsArray);
6392
6393
	// PHP 5.3 so no json_last_error_msg()
6394
	switch (json_last_error())
6395
	{
6396
		case JSON_ERROR_NONE:
6397
			$jsonError = false;
6398
			break;
6399
		case JSON_ERROR_DEPTH:
6400
			$jsonError = 'JSON_ERROR_DEPTH';
6401
			break;
6402
		case JSON_ERROR_STATE_MISMATCH:
6403
			$jsonError = 'JSON_ERROR_STATE_MISMATCH';
6404
			break;
6405
		case JSON_ERROR_CTRL_CHAR:
6406
			$jsonError = 'JSON_ERROR_CTRL_CHAR';
6407
			break;
6408
		case JSON_ERROR_SYNTAX:
6409
			$jsonError = 'JSON_ERROR_SYNTAX';
6410
			break;
6411
		case JSON_ERROR_UTF8:
6412
			$jsonError = 'JSON_ERROR_UTF8';
6413
			break;
6414
		default:
6415
			$jsonError = 'unknown';
6416
			break;
6417
	}
6418
6419
	// Something went wrong!
6420
	if (!empty($jsonError) && $logIt)
6421
	{
6422
		// Being a wrapper means we lost our smf_error_handler() privileges :(
6423
		$jsonDebug = debug_backtrace();
6424
		$jsonDebug = $jsonDebug[0];
6425
		loadLanguage('Errors');
6426
6427
		if (!empty($jsonDebug))
6428
			log_error($txt['json_' . $jsonError], 'critical', $jsonDebug['file'], $jsonDebug['line']);
6429
6430
		else
6431
			log_error($txt['json_' . $jsonError], 'critical');
6432
6433
		// Everyone expects an array.
6434
		return array();
6435
	}
6436
6437
	return $returnArray;
6438
}
6439
6440
/**
6441
 * Check the given String if he is a valid IPv4 or IPv6
6442
 * return true or false
6443
 *
6444
 * @param string $IPString
6445
 *
6446
 * @return bool
6447
 */
6448
function isValidIP($IPString)
6449
{
6450
	return filter_var($IPString, FILTER_VALIDATE_IP) !== false;
6451
}
6452
6453
/**
6454
 * Outputs a response.
6455
 * It assumes the data is already a string.
6456
 *
6457
 * @param string $data The data to print
6458
 * @param string $type The content type. Defaults to Json.
6459
 * @return void
6460
 */
6461
function smf_serverResponse($data = '', $type = 'content-type: application/json')
6462
{
6463
	global $db_show_debug, $modSettings;
6464
6465
	// Defensive programming anyone?
6466
	if (empty($data))
6467
		return false;
6468
6469
	// Don't need extra stuff...
6470
	$db_show_debug = false;
6471
6472
	// Kill anything else.
6473
	ob_end_clean();
6474
6475
	if (!empty($modSettings['CompressedOutput']))
6476
		@ob_start('ob_gzhandler');
6477
6478
	else
6479
		ob_start();
6480
6481
	// Set the header.
6482
	header($type);
6483
6484
	// Echo!
6485
	echo $data;
6486
6487
	// Done.
6488
	obExit(false);
6489
}
6490
6491
/**
6492
 * Creates an optimized regex to match all known top level domains.
6493
 *
6494
 * The optimized regex is stored in $modSettings['tld_regex'].
6495
 *
6496
 * To update the stored version of the regex to use the latest list of valid
6497
 * TLDs from iana.org, set the $update parameter to true. Updating can take some
6498
 * time, based on network connectivity, so it should normally only be done by
6499
 * calling this function from a background or scheduled task.
6500
 *
6501
 * If $update is not true, but the regex is missing or invalid, the regex will
6502
 * be regenerated from a hard-coded list of TLDs. This regenerated regex will be
6503
 * overwritten on the next scheduled update.
6504
 *
6505
 * @param bool $update If true, fetch and process the latest official list of TLDs from iana.org.
6506
 */
6507
function set_tld_regex($update = false)
6508
{
6509
	global $sourcedir, $smcFunc, $modSettings;
6510
	static $done = false;
6511
6512
	// If we don't need to do anything, don't
6513
	if (!$update && $done)
6514
		return;
6515
6516
	// Should we get a new copy of the official list of TLDs?
6517
	if ($update)
6518
	{
6519
		$tlds = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt');
6520
		$tlds_md5 = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt.md5');
6521
6522
		/**
6523
		 * If the Internet Assigned Numbers Authority can't be reached, the Internet is GONE!
6524
		 * We're probably running on a server hidden in a bunker deep underground to protect
6525
		 * it from marauding bandits roaming on the surface. We don't want to waste precious
6526
		 * electricity on pointlessly repeating background tasks, so we'll wait until the next
6527
		 * regularly scheduled update to see if civilization has been restored.
6528
		 */
6529
		if ($tlds === false || $tlds_md5 === false)
6530
			$postapocalypticNightmare = true;
6531
6532
		// Make sure nothing went horribly wrong along the way.
6533
		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

6533
		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

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

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

7267
				$value = substr(/** @scrutinizer ignore-type */ $value, 0, $param_max - strlen($key) - 5);
Loading history...
Bug introduced by
$param_max - strlen($key) - 5 of type double is incompatible with the type integer 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

7267
				$value = substr($value, 0, /** @scrutinizer ignore-type */ $param_max - strlen($key) - 5);
Loading history...
7268
		}
7269
7270
		return $array;
7271
	}
7272
}
7273
7274
/**
7275
 * array_length Recursive
7276
 * @param array $array
7277
 * @param int $deep How many levels should the function
7278
 * @return int
7279
 */
7280
function array_length($array, $deep = 3)
7281
{
7282
	// Work with arrays
7283
	$array = (array) $array;
7284
	$length = 0;
7285
7286
	$deep_count = $deep - 1;
7287
7288
	foreach ($array as $value)
7289
	{
7290
		// Recursive?
7291
		if (is_array($value))
7292
		{
7293
			// No can't do
7294
			if ($deep_count <= 0)
7295
				continue;
7296
7297
			$length += array_length($value, $deep_count);
7298
		}
7299
		else
7300
			$length += strlen($value);
7301
	}
7302
7303
	return $length;
7304
}
7305
7306
/**
7307
 * Compares existance request variables against an array.
7308
 *
7309
 * The input array is associative, where keys denote accepted values
7310
 * in a request variable denoted by `$req_val`. Values can be:
7311
 *
7312
 * - another associative array where at least one key must be found
7313
 *   in the request and their values are accepted request values.
7314
 * - A scalar value, in which case no furthur checks are done.
7315
 *
7316
 * @param array $array
7317
 * @param string $req_var request variable
7318
 *
7319
 * @return bool whether any of the criteria was satisfied
7320
 */
7321
function is_filtered_request(array $array, $req_var)
7322
{
7323
	$matched = false;
7324
	if (isset($_REQUEST[$req_var], $array[$_REQUEST[$req_var]]))
7325
	{
7326
		if (is_array($array[$_REQUEST[$req_var]]))
7327
		{
7328
			foreach ($array[$_REQUEST[$req_var]] as $subtype => $subnames)
7329
				$matched |= isset($_REQUEST[$subtype]) && in_array($_REQUEST[$subtype], $subnames);
7330
		}
7331
		else
7332
			$matched = true;
7333
	}
7334
7335
	return (bool) $matched;
7336
}
7337
7338
/**
7339
 * Clean up the XML to make sure it doesn't contain invalid characters.
7340
 *
7341
 * See https://www.w3.org/TR/xml/#charsets
7342
 *
7343
 * @param string $string The string to clean
7344
 * @return string The cleaned string
7345
 */
7346
function cleanXml($string)
7347
{
7348
	global $context;
7349
7350
	$illegal_chars = array(
7351
		// Remove all ASCII control characters except \t, \n, and \r.
7352
		"\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08",
7353
		"\x0B", "\x0C", "\x0E", "\x0F", "\x10", "\x11", "\x12", "\x13", "\x14",
7354
		"\x15", "\x16", "\x17", "\x18", "\x19", "\x1A", "\x1B", "\x1C", "\x1D",
7355
		"\x1E", "\x1F",
7356
		// Remove \xFFFE and \xFFFF
7357
		"\xEF\xBF\xBE", "\xEF\xBF\xBF",
7358
	);
7359
7360
	$string = str_replace($illegal_chars, '', $string);
7361
7362
	// The Unicode surrogate pair code points should never be present in our
7363
	// strings to begin with, but if any snuck in, they need to be removed.
7364
	if (!empty($context['utf8']) && strpos($string, "\xED") !== false)
7365
		$string = preg_replace('/\xED[\xA0-\xBF][\x80-\xBF]/', '', $string);
7366
7367
	return $string;
7368
}
7369
7370
/**
7371
 * Escapes (replaces) characters in strings to make them safe for use in javascript
7372
 *
7373
 * @param string $string The string to escape
7374
 * @return string The escaped string
7375
 */
7376
function JavaScriptEscape($string)
7377
{
7378
	global $scripturl;
7379
7380
	return '\'' . strtr($string, array(
7381
		"\r" => '',
7382
		"\n" => '\\n',
7383
		"\t" => '\\t',
7384
		'\\' => '\\\\',
7385
		'\'' => '\\\'',
7386
		'</' => '<\' + \'/',
7387
		'<script' => '<scri\'+\'pt',
7388
		'<body>' => '<bo\'+\'dy>',
7389
		'<a href' => '<a hr\'+\'ef',
7390
		$scripturl => '\' + smf_scripturl + \'',
7391
	)) . '\'';
7392
}
7393
7394
?>