Passed
Push — release-2.1 ( 41509f...c9f0cf )
by Mathias
06:43
created

safe_unserialize()   A

Complexity

Conditions 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

2266
		$alltags_regex = '(?' . '>\b' . /** @scrutinizer ignore-type */ build_regex(array_unique($alltags)) . '\b|' . build_regex(array_keys($itemcodes)) . ')';
Loading history...
2267
	}
2268
2269
	$pos = -1;
2270
	while ($pos !== false)
2271
	{
2272
		$last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos;
2273
		preg_match('~\[/?(?=' . $alltags_regex . ')~i', $message, $matches, PREG_OFFSET_CAPTURE, $pos + 1);
2274
		$pos = isset($matches[0][1]) ? $matches[0][1] : false;
2275
2276
		// Failsafe.
2277
		if ($pos === false || $last_pos > $pos)
2278
			$pos = strlen($message) + 1;
2279
2280
		// Can't have a one letter smiley, URL, or email! (Sorry.)
2281
		if ($last_pos < $pos - 1)
2282
		{
2283
			// Make sure the $last_pos is not negative.
2284
			$last_pos = max($last_pos, 0);
2285
2286
			// Pick a block of data to do some raw fixing on.
2287
			$data = substr($message, $last_pos, $pos - $last_pos);
2288
2289
			$placeholders = array();
2290
			$placeholders_counter = 0;
2291
2292
			// Take care of some HTML!
2293
			if (!empty($modSettings['enablePostHTML']) && strpos($data, '&lt;') !== false)
2294
			{
2295
				$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);
2296
2297
				// <br> should be empty.
2298
				$empty_tags = array('br', 'hr');
2299
				foreach ($empty_tags as $tag)
2300
					$data = str_replace(array('&lt;' . $tag . '&gt;', '&lt;' . $tag . '/&gt;', '&lt;' . $tag . ' /&gt;'), '<' . $tag . '>', $data);
2301
2302
				// b, u, i, s, pre... basic tags.
2303
				$closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote', 'strong');
2304
				foreach ($closable_tags as $tag)
2305
				{
2306
					$diff = substr_count($data, '&lt;' . $tag . '&gt;') - substr_count($data, '&lt;/' . $tag . '&gt;');
2307
					$data = strtr($data, array('&lt;' . $tag . '&gt;' => '<' . $tag . '>', '&lt;/' . $tag . '&gt;' => '</' . $tag . '>'));
2308
2309
					if ($diff > 0)
2310
						$data = substr($data, 0, -1) . str_repeat('</' . $tag . '>', $diff) . substr($data, -1);
2311
				}
2312
2313
				// Do <img ...> - with security... action= -> action-.
2314
				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);
2315
				if (!empty($matches[0]))
2316
				{
2317
					$replaces = array();
2318
					foreach ($matches[2] as $match => $imgtag)
2319
					{
2320
						$alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^&quot;|&quot;$~', '', $matches[3][$match]);
2321
2322
						// Remove action= from the URL - no funny business, now.
2323
						// @todo Testing this preg_match seems pointless
2324
						if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) != 0)
2325
							$imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag);
2326
2327
						$placeholder = '<placeholder ' . ++$placeholders_counter . '>';
2328
						$placeholders[$placeholder] = '[img' . $alt . ']' . $imgtag . '[/img]';
2329
2330
						$replaces[$matches[0][$match]] = $placeholder;
2331
					}
2332
2333
					$data = strtr($data, $replaces);
2334
				}
2335
			}
2336
2337
			if (!empty($modSettings['autoLinkUrls']))
2338
			{
2339
				// Are we inside tags that should be auto linked?
2340
				$no_autolink_area = false;
2341
				if (!empty($open_tags))
2342
				{
2343
					foreach ($open_tags as $open_tag)
2344
						if (in_array($open_tag['tag'], $no_autolink_tags))
2345
							$no_autolink_area = true;
2346
				}
2347
2348
				// Don't go backwards.
2349
				// @todo Don't think is the real solution....
2350
				$lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0;
2351
				if ($pos < $lastAutoPos)
2352
					$no_autolink_area = true;
2353
				$lastAutoPos = $pos;
2354
2355
				if (!$no_autolink_area)
2356
				{
2357
					// An &nbsp; right after a URL can break the autolinker
2358
					if (strpos($data, '&nbsp;') !== false)
2359
					{
2360
						$placeholders['<placeholder non-breaking-space>'] = '&nbsp;';
2361
						$data = strtr($data, array('&nbsp;' => '<placeholder non-breaking-space>'));
2362
					}
2363
2364
					// Parse any URLs
2365
					if (!isset($disabled['url']) && strpos($data, '[url') === false)
2366
					{
2367
						// For efficiency, first define the TLD regex in a PCRE subroutine
2368
						$url_regex = '(?(DEFINE)(?<tlds>' . $modSettings['tld_regex'] . '))';
2369
2370
						// Now build the rest of the regex
2371
						$url_regex .=
2372
						// 1. IRI scheme and domain components
2373
						'(?:' .
2374
							// 1a. IRIs with a scheme, or at least an opening "//"
2375
							'(?:' .
2376
2377
								// URI scheme (or lack thereof for schemeless URLs)
2378
								'(?:' .
2379
									// URL scheme and colon
2380
									'\b[a-z][\w\-]+:' .
2381
									// or
2382
									'|' .
2383
									// A boundary followed by two slashes for schemeless URLs
2384
									'(?<=^|\W)(?=//)' .
2385
								')' .
2386
2387
								// IRI "authority" chunk
2388
								'(?:' .
2389
									// 2 slashes for IRIs with an "authority"
2390
									'//' .
2391
									// then a domain name
2392
									'(?:' .
2393
										// Either the reserved "localhost" domain name
2394
										'localhost' .
2395
										// or
2396
										'|' .
2397
										// a run of IRI characters, a dot, and a TLD
2398
										'[\p{L}\p{M}\p{N}\-.:@]+\.(?P>tlds)' .
2399
									')' .
2400
									// followed by a non-domain character or end of line
2401
									'(?=[^\p{L}\p{N}\-.]|$)' .
2402
2403
									// or, if no "authority" per se (e.g. "mailto:" URLs)...
2404
									'|' .
2405
2406
									// a run of IRI characters
2407
									'[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.:@]+[\p{L}\p{M}\p{N}]' .
2408
									// and then a dot and a closing IRI label
2409
									'\.[\p{L}\p{M}\p{N}\-]+' .
2410
								')' .
2411
							')' .
2412
2413
							// Or
2414
							'|' .
2415
2416
							// 1b. Naked domains (e.g. "example.com" in "Go to example.com for an example.")
2417
							'(?:' .
2418
								// Preceded by start of line or a non-domain character
2419
								'(?<=^|[^\p{L}\p{M}\p{N}\-:@])' .
2420
								// A run of Unicode domain name characters (excluding [:@])
2421
								'[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.]+[\p{L}\p{M}\p{N}]' .
2422
								// and then a dot and a valid TLD
2423
								'\.(?P>tlds)' .
2424
								// Followed by either:
2425
								'(?=' .
2426
									// end of line or a non-domain character (excluding [.:@])
2427
									'$|[^\p{L}\p{N}\-]' .
2428
									// or
2429
									'|' .
2430
									// a dot followed by end of line or a non-domain character (excluding [.:@])
2431
									'\.(?=$|[^\p{L}\p{N}\-])' .
2432
								')' .
2433
							')' .
2434
						')' .
2435
2436
						// 2. IRI path, query, and fragment components (if present)
2437
						'(?:' .
2438
2439
							// If any of these parts exist, must start with a single "/"
2440
							'/' .
2441
2442
							// And then optionally:
2443
							'(?:' .
2444
								// One or more of:
2445
								'(?:' .
2446
									// a run of non-space, non-()<>
2447
									'[^\s()<>]+' .
2448
									// or
2449
									'|' .
2450
									// balanced parentheses, up to 2 levels
2451
									'\(([^\s()<>]+|(\([^\s()<>]+\)))*\)' .
2452
								')+' .
2453
								// Ending with:
2454
								'(?:' .
2455
									// balanced parentheses, up to 2 levels
2456
									'\(([^\s()<>]+|(\([^\s()<>]+\)))*\)' .
2457
									// or
2458
									'|' .
2459
									// not a space or one of these punctuation characters
2460
									'[^\s`!()\[\]{};:\'".,<>?«»“”‘’/]' .
2461
									// or
2462
									'|' .
2463
									// a trailing slash (but not two in a row)
2464
									'(?<!/)/' .
2465
								')' .
2466
							')?' .
2467
						')?';
2468
2469
						$data = preg_replace_callback('~' . $url_regex . '~i' . ($context['utf8'] ? 'u' : ''), function($matches)
2470
						{
2471
							$url = array_shift($matches);
2472
2473
							// If this isn't a clean URL, bail out
2474
							if ($url != sanitize_iri($url))
2475
								return $url;
2476
2477
							$scheme = parse_url($url, PHP_URL_SCHEME);
2478
2479
							if ($scheme == 'mailto')
2480
							{
2481
								$email_address = str_replace('mailto:', '', $url);
2482
								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...
2483
									return '[email=' . $email_address . ']' . $url . '[/email]';
2484
								else
2485
									return $url;
2486
							}
2487
2488
							// Are we linking a schemeless URL or naked domain name (e.g. "example.com")?
2489
							if (empty($scheme))
2490
								$fullUrl = '//' . ltrim($url, ':/');
2491
							else
2492
								$fullUrl = $url;
2493
2494
							// Make sure that $fullUrl really is valid
2495
							if (validate_iri((strpos($fullUrl, '//') === 0 ? 'http:' : '') . $fullUrl) === false)
2496
								return $url;
2497
2498
							return '[url=&quot;' . str_replace(array('[', ']'), array('&#91;', '&#93;'), $fullUrl) . '&quot;]' . $url . '[/url]';
2499
						}, $data);
2500
					}
2501
2502
					// Next, emails...  Must be careful not to step on enablePostHTML logic above...
2503
					if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false && stripos($data, 'mailto:') === false)
2504
					{
2505
						$email_regex = '
2506
						# Preceded by a non-domain character or start of line
2507
						(?<=^|[^\p{L}\p{M}\p{N}\-\.])
2508
2509
						# An email address
2510
						[\p{L}\p{M}\p{N}_\-.]{1,80}
2511
						@
2512
						[\p{L}\p{M}\p{N}\-.]+
2513
						\.
2514
						' . $modSettings['tld_regex'] . '
2515
2516
						# Followed by either:
2517
						(?=
2518
							# end of line or a non-domain character (excluding the dot)
2519
							$|[^\p{L}\p{M}\p{N}\-]
2520
							| # or
2521
							# a dot followed by end of line or a non-domain character
2522
							\.(?=$|[^\p{L}\p{M}\p{N}\-])
2523
						)';
2524
2525
						$data = preg_replace('~' . $email_regex . '~xi' . ($context['utf8'] ? 'u' : ''), '[email]$0[/email]', $data);
2526
					}
2527
				}
2528
			}
2529
2530
			// Restore any placeholders
2531
			$data = strtr($data, $placeholders);
2532
2533
			$data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));
2534
2535
			// If it wasn't changed, no copying or other boring stuff has to happen!
2536
			if ($data != substr($message, $last_pos, $pos - $last_pos))
2537
			{
2538
				$message = substr($message, 0, $last_pos) . $data . substr($message, $pos);
2539
2540
				// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
2541
				$old_pos = strlen($data) + $last_pos;
2542
				$pos = strpos($message, '[', $last_pos);
2543
				$pos = $pos === false ? $old_pos : min($pos, $old_pos);
2544
			}
2545
		}
2546
2547
		// Are we there yet?  Are we there yet?
2548
		if ($pos >= strlen($message) - 1)
2549
			break;
2550
2551
		$tag_character = strtolower($message[$pos + 1]);
2552
2553
		if ($tag_character == '/' && !empty($open_tags))
2554
		{
2555
			$pos2 = strpos($message, ']', $pos + 1);
2556
			if ($pos2 == $pos + 2)
2557
				continue;
2558
2559
			$look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));
2560
2561
			// A closing tag that doesn't match any open tags? Skip it.
2562
			if (!in_array($look_for, array_map(function($code)
2563
			{
2564
				return $code['tag'];
2565
			}, $open_tags)))
2566
				continue;
2567
2568
			$to_close = array();
2569
			$block_level = null;
2570
2571
			do
2572
			{
2573
				$tag = array_pop($open_tags);
2574
				if (!$tag)
2575
					break;
2576
2577
				if (!empty($tag['block_level']))
2578
				{
2579
					// Only find out if we need to.
2580
					if ($block_level === false)
2581
					{
2582
						array_push($open_tags, $tag);
2583
						break;
2584
					}
2585
2586
					// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
2587
					if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]]))
2588
					{
2589
						foreach ($bbc_codes[$look_for[0]] as $temp)
2590
							if ($temp['tag'] == $look_for)
2591
							{
2592
								$block_level = !empty($temp['block_level']);
2593
								break;
2594
							}
2595
					}
2596
2597
					if ($block_level !== true)
2598
					{
2599
						$block_level = false;
2600
						array_push($open_tags, $tag);
2601
						break;
2602
					}
2603
				}
2604
2605
				$to_close[] = $tag;
2606
			}
2607
			while ($tag['tag'] != $look_for);
2608
2609
			// Did we just eat through everything and not find it?
2610
			if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
2611
			{
2612
				$open_tags = $to_close;
2613
				continue;
2614
			}
2615
			elseif (!empty($to_close) && $tag['tag'] != $look_for)
2616
			{
2617
				if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]]))
2618
				{
2619
					foreach ($bbc_codes[$look_for[0]] as $temp)
2620
						if ($temp['tag'] == $look_for)
2621
						{
2622
							$block_level = !empty($temp['block_level']);
2623
							break;
2624
						}
2625
				}
2626
2627
				// We're not looking for a block level tag (or maybe even a tag that exists...)
2628
				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...
2629
				{
2630
					foreach ($to_close as $tag)
2631
						array_push($open_tags, $tag);
2632
					continue;
2633
				}
2634
			}
2635
2636
			foreach ($to_close as $tag)
2637
			{
2638
				$message = substr($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1);
2639
				$pos += strlen($tag['after']) + 2;
2640
				$pos2 = $pos - 1;
2641
2642
				// See the comment at the end of the big loop - just eating whitespace ;).
2643
				$whitespace_regex = '';
2644
				if (!empty($tag['block_level']))
2645
					$whitespace_regex .= '(&nbsp;|\s)*(<br\s*/?' . '>)?';
2646
				// Trim one line of whitespace after unnested tags, but all of it after nested ones
2647
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
2648
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2649
2650
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
2651
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2652
			}
2653
2654
			if (!empty($to_close))
2655
			{
2656
				$to_close = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $to_close is dead and can be removed.
Loading history...
2657
				$pos--;
2658
			}
2659
2660
			continue;
2661
		}
2662
2663
		// No tags for this character, so just keep going (fastest possible course.)
2664
		if (!isset($bbc_codes[$tag_character]))
2665
			continue;
2666
2667
		$inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
2668
		$tag = null;
2669
		foreach ($bbc_codes[$tag_character] as $possible)
2670
		{
2671
			$pt_strlen = strlen($possible['tag']);
2672
2673
			// Not a match?
2674
			if (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag'])
2675
				continue;
2676
2677
			$next_c = isset($message[$pos + 1 + $pt_strlen]) ? $message[$pos + 1 + $pt_strlen] : '';
2678
2679
			// A tag is the last char maybe
2680
			if ($next_c == '')
2681
				break;
2682
2683
			// A test validation?
2684
			if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0)
2685
				continue;
2686
			// Do we want parameters?
2687
			elseif (!empty($possible['parameters']))
2688
			{
2689
				// Are all the parameters optional?
2690
				$param_required = false;
2691
				foreach ($possible['parameters'] as $param)
2692
				{
2693
					if (empty($param['optional']))
2694
					{
2695
						$param_required = true;
2696
						break;
2697
					}
2698
				}
2699
2700
				if ($param_required && $next_c != ' ')
2701
					continue;
2702
			}
2703
			elseif (isset($possible['type']))
2704
			{
2705
				// Do we need an equal sign?
2706
				if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=')
2707
					continue;
2708
				// Maybe we just want a /...
2709
				if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + $pt_strlen, 2) != '/]' && substr($message, $pos + 1 + $pt_strlen, 3) != ' /]')
2710
					continue;
2711
				// An immediate ]?
2712
				if ($possible['type'] == 'unparsed_content' && $next_c != ']')
2713
					continue;
2714
			}
2715
			// No type means 'parsed_content', which demands an immediate ] without parameters!
2716
			elseif ($next_c != ']')
2717
				continue;
2718
2719
			// Check allowed tree?
2720
			if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
2721
				continue;
2722
			elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
2723
				continue;
2724
			// If this is in the list of disallowed child tags, don't parse it.
2725
			elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
2726
				continue;
2727
2728
			$pos1 = $pos + 1 + $pt_strlen + 1;
2729
2730
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
2731
			if ($possible['tag'] == 'quote')
2732
			{
2733
				// Start with standard
2734
				$quote_alt = false;
2735
				foreach ($open_tags as $open_quote)
2736
				{
2737
					// Every parent quote this quote has flips the styling
2738
					if ($open_quote['tag'] == 'quote')
2739
						$quote_alt = !$quote_alt;
0 ignored issues
show
introduced by
The condition $quote_alt is always false.
Loading history...
2740
				}
2741
				// Add a class to the quote to style alternating blockquotes
2742
				$possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">'));
2743
			}
2744
2745
			// This is long, but it makes things much easier and cleaner.
2746
			if (!empty($possible['parameters']))
2747
			{
2748
				// Build a regular expression for each parameter for the current tag.
2749
				$regex_key = $smcFunc['json_encode']($possible['parameters']);
2750
				if (!isset($params_regexes[$regex_key]))
2751
				{
2752
					$params_regexes[$regex_key] = '';
2753
2754
					foreach ($possible['parameters'] as $p => $info)
2755
						$params_regexes[$regex_key] .= '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . '\s*)' . (empty($info['optional']) ? '' : '?');
2756
				}
2757
2758
				// Extract the string that potentially holds our parameters.
2759
				$blob = preg_split('~\[/?(?:' . $alltags_regex . ')~i', substr($message, $pos));
2760
				$blobs = preg_split('~\]~i', $blob[1]);
2761
2762
				$splitters = implode('=|', array_keys($possible['parameters'])) . '=';
2763
2764
				// Progressively append more blobs until we find our parameters or run out of blobs
2765
				$blob_counter = 1;
2766
				while ($blob_counter <= count($blobs))
2767
				{
2768
					$given_param_string = implode(']', array_slice($blobs, 0, $blob_counter++));
2769
2770
					$given_params = preg_split('~\s(?=(' . $splitters . '))~i', $given_param_string);
2771
					sort($given_params, SORT_STRING);
2772
2773
					$match = preg_match('~^' . $params_regexes[$regex_key] . '$~i', implode(' ', $given_params), $matches) !== 0;
2774
2775
					if ($match)
2776
						break;
2777
				}
2778
2779
				// Didn't match our parameter list, try the next possible.
2780
				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...
2781
					continue;
2782
2783
				$params = array();
2784
				for ($i = 1, $n = count($matches); $i < $n; $i += 2)
2785
				{
2786
					$key = strtok(ltrim($matches[$i]), '=');
2787
					if ($key === false)
2788
						continue;
2789
					elseif (isset($possible['parameters'][$key]['value']))
2790
						$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
2791
					elseif (isset($possible['parameters'][$key]['validate']))
2792
						$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
2793
					else
2794
						$params['{' . $key . '}'] = $matches[$i + 1];
2795
2796
					// Just to make sure: replace any $ or { so they can't interpolate wrongly.
2797
					$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
2798
				}
2799
2800
				foreach ($possible['parameters'] as $p => $info)
2801
				{
2802
					if (!isset($params['{' . $p . '}']))
2803
					{
2804
						if (!isset($info['default']))
2805
							$params['{' . $p . '}'] = '';
2806
						elseif (isset($possible['parameters'][$p]['value']))
2807
							$params['{' . $p . '}'] = strtr($possible['parameters'][$p]['value'], array('$1' => $info['default']));
2808
						elseif (isset($possible['parameters'][$p]['validate']))
2809
							$params['{' . $p . '}'] = $possible['parameters'][$p]['validate']($info['default']);
2810
						else
2811
							$params['{' . $p . '}'] = $info['default'];
2812
					}
2813
				}
2814
2815
				$tag = $possible;
2816
2817
				// Put the parameters into the string.
2818
				if (isset($tag['before']))
2819
					$tag['before'] = strtr($tag['before'], $params);
2820
				if (isset($tag['after']))
2821
					$tag['after'] = strtr($tag['after'], $params);
2822
				if (isset($tag['content']))
2823
					$tag['content'] = strtr($tag['content'], $params);
2824
2825
				$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...
2826
			}
2827
			else
2828
			{
2829
				$tag = $possible;
2830
				$params = array();
2831
			}
2832
			break;
2833
		}
2834
2835
		// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
2836
		if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
2837
		{
2838
			if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
2839
				continue;
2840
2841
			$tag = $itemcodes[$message[$pos + 1]];
2842
2843
			// First let's set up the tree: it needs to be in a list, or after an li.
2844
			if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
2845
			{
2846
				$open_tags[] = array(
2847
					'tag' => 'list',
2848
					'after' => '</ul>',
2849
					'block_level' => true,
2850
					'require_children' => array('li'),
2851
					'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2852
				);
2853
				$code = '<ul class="bbc_list">';
2854
			}
2855
			// We're in a list item already: another itemcode?  Close it first.
2856
			elseif ($inside['tag'] == 'li')
2857
			{
2858
				array_pop($open_tags);
2859
				$code = '</li>';
2860
			}
2861
			else
2862
				$code = '';
2863
2864
			// Now we open a new tag.
2865
			$open_tags[] = array(
2866
				'tag' => 'li',
2867
				'after' => '</li>',
2868
				'trim' => 'outside',
2869
				'block_level' => true,
2870
				'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2871
			);
2872
2873
			// First, open the tag...
2874
			$code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
2875
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
2876
			$pos += strlen($code) - 1 + 2;
2877
2878
			// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
2879
			$pos2 = strpos($message, '<br>', $pos);
2880
			$pos3 = strpos($message, '[/', $pos);
2881
			if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
2882
			{
2883
				preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 4), $matches);
2884
				$message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
2885
2886
				$open_tags[count($open_tags) - 2]['after'] = '</ul>';
2887
			}
2888
			// Tell the [list] that it needs to close specially.
2889
			else
2890
			{
2891
				// Move the li over, because we're not sure what we'll hit.
2892
				$open_tags[count($open_tags) - 1]['after'] = '';
2893
				$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
2894
			}
2895
2896
			continue;
2897
		}
2898
2899
		// Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
2900
		if ($tag === null && $inside !== null && !empty($inside['require_children']))
2901
		{
2902
			array_pop($open_tags);
2903
2904
			$message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
2905
			$pos += strlen($inside['after']) - 1 + 2;
2906
		}
2907
2908
		// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
2909
		if ($tag === null)
2910
			continue;
2911
2912
		// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
2913
		if (isset($inside['disallow_children']))
2914
			$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
2915
2916
		// Is this tag disabled?
2917
		if (isset($disabled[$tag['tag']]))
2918
		{
2919
			if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
2920
			{
2921
				$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
2922
				$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
2923
				$tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
2924
			}
2925
			elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
2926
			{
2927
				$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
2928
				$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
2929
			}
2930
			else
2931
				$tag['content'] = $tag['disabled_content'];
2932
		}
2933
2934
		// we use this a lot
2935
		$tag_strlen = strlen($tag['tag']);
2936
2937
		// The only special case is 'html', which doesn't need to close things.
2938
		if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
2939
		{
2940
			$n = count($open_tags) - 1;
2941
			while (empty($open_tags[$n]['block_level']) && $n >= 0)
2942
				$n--;
2943
2944
			// Close all the non block level tags so this tag isn't surrounded by them.
2945
			for ($i = count($open_tags) - 1; $i > $n; $i--)
2946
			{
2947
				$message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
2948
				$ot_strlen = strlen($open_tags[$i]['after']);
2949
				$pos += $ot_strlen + 2;
2950
				$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...
2951
2952
				// Trim or eat trailing stuff... see comment at the end of the big loop.
2953
				$whitespace_regex = '';
2954
				if (!empty($tag['block_level']))
2955
					$whitespace_regex .= '(&nbsp;|\s)*(<br>)?';
2956
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
2957
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2958
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
2959
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2960
2961
				array_pop($open_tags);
2962
			}
2963
		}
2964
2965
		// Can't read past the end of the message
2966
		$pos1 = min(strlen($message), $pos1);
2967
2968
		// No type means 'parsed_content'.
2969
		if (!isset($tag['type']))
2970
		{
2971
			$open_tags[] = $tag;
2972
			$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
2973
			$pos += strlen($tag['before']) - 1 + 2;
2974
		}
2975
		// Don't parse the content, just skip it.
2976
		elseif ($tag['type'] == 'unparsed_content')
2977
		{
2978
			$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
2979
			if ($pos2 === false)
2980
				continue;
2981
2982
			$data = substr($message, $pos1, $pos2 - $pos1);
2983
2984
			if (!empty($tag['block_level']) && substr($data, 0, 4) == '<br>')
2985
				$data = substr($data, 4);
2986
2987
			if (isset($tag['validate']))
2988
				$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...
2989
2990
			$code = strtr($tag['content'], array('$1' => $data));
2991
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
2992
2993
			$pos += strlen($code) - 1 + 2;
2994
			$last_pos = $pos + 1;
2995
		}
2996
		// Don't parse the content, just skip it.
2997
		elseif ($tag['type'] == 'unparsed_equals_content')
2998
		{
2999
			// The value may be quoted for some tags - check.
3000
			if (isset($tag['quoted']))
3001
			{
3002
				$quoted = substr($message, $pos1, 6) == '&quot;';
3003
				if ($tag['quoted'] != 'optional' && !$quoted)
3004
					continue;
3005
3006
				if ($quoted)
3007
					$pos1 += 6;
3008
			}
3009
			else
3010
				$quoted = false;
3011
3012
			$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...
3013
			if ($pos2 === false)
3014
				continue;
3015
3016
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3017
			if ($pos3 === false)
3018
				continue;
3019
3020
			$data = array(
3021
				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...
3022
				substr($message, $pos1, $pos2 - $pos1)
3023
			);
3024
3025
			if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '<br>')
3026
				$data[0] = substr($data[0], 4);
3027
3028
			// Validation for my parking, please!
3029
			if (isset($tag['validate']))
3030
				$tag['validate']($tag, $data, $disabled, $params);
3031
3032
			$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
3033
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3034
			$pos += strlen($code) - 1 + 2;
3035
		}
3036
		// A closed tag, with no content or value.
3037
		elseif ($tag['type'] == 'closed')
3038
		{
3039
			$pos2 = strpos($message, ']', $pos);
3040
			$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
3041
			$pos += strlen($tag['content']) - 1 + 2;
3042
		}
3043
		// This one is sorta ugly... :/.  Unfortunately, it's needed for flash.
3044
		elseif ($tag['type'] == 'unparsed_commas_content')
3045
		{
3046
			$pos2 = strpos($message, ']', $pos1);
3047
			if ($pos2 === false)
3048
				continue;
3049
3050
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3051
			if ($pos3 === false)
3052
				continue;
3053
3054
			// We want $1 to be the content, and the rest to be csv.
3055
			$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
3056
			$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
3057
3058
			if (isset($tag['validate']))
3059
				$tag['validate']($tag, $data, $disabled, $params);
3060
3061
			$code = $tag['content'];
3062
			foreach ($data as $k => $d)
3063
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3064
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3065
			$pos += strlen($code) - 1 + 2;
3066
		}
3067
		// This has parsed content, and a csv value which is unparsed.
3068
		elseif ($tag['type'] == 'unparsed_commas')
3069
		{
3070
			$pos2 = strpos($message, ']', $pos1);
3071
			if ($pos2 === false)
3072
				continue;
3073
3074
			$data = explode(',', substr($message, $pos1, $pos2 - $pos1));
3075
3076
			if (isset($tag['validate']))
3077
				$tag['validate']($tag, $data, $disabled, $params);
3078
3079
			// Fix after, for disabled code mainly.
3080
			foreach ($data as $k => $d)
3081
				$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
3082
3083
			$open_tags[] = $tag;
3084
3085
			// Replace them out, $1, $2, $3, $4, etc.
3086
			$code = $tag['before'];
3087
			foreach ($data as $k => $d)
3088
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3089
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1);
3090
			$pos += strlen($code) - 1 + 2;
3091
		}
3092
		// A tag set to a value, parsed or not.
3093
		elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
3094
		{
3095
			// The value may be quoted for some tags - check.
3096
			if (isset($tag['quoted']))
3097
			{
3098
				$quoted = substr($message, $pos1, 6) == '&quot;';
3099
				if ($tag['quoted'] != 'optional' && !$quoted)
3100
					continue;
3101
3102
				if ($quoted)
3103
					$pos1 += 6;
3104
			}
3105
			else
3106
				$quoted = false;
3107
3108
			if ($quoted)
3109
			{
3110
				$end_of_value = strpos($message, '&quot;]', $pos1);
3111
				$nested_tag = strpos($message, '=&quot;', $pos1);
3112
				if ($nested_tag && $nested_tag < $end_of_value)
3113
					// Nested tag with quoted value detected, use next end tag
3114
					$nested_tag_pos = strpos($message, $quoted == false ? ']' : '&quot;]', $pos1) + 6;
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
3115
			}
3116
3117
			$pos2 = strpos($message, $quoted == false ? ']' : '&quot;]', isset($nested_tag_pos) ? $nested_tag_pos : $pos1);
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
3118
			if ($pos2 === false)
3119
				continue;
3120
3121
			$data = substr($message, $pos1, $pos2 - $pos1);
3122
3123
			// Validation for my parking, please!
3124
			if (isset($tag['validate']))
3125
				$tag['validate']($tag, $data, $disabled, $params);
3126
3127
			// For parsed content, we must recurse to avoid security problems.
3128
			if ($tag['type'] != 'unparsed_equals')
3129
				$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
3130
3131
			$tag['after'] = strtr($tag['after'], array('$1' => $data));
3132
3133
			$open_tags[] = $tag;
3134
3135
			$code = strtr($tag['before'], array('$1' => $data));
3136
			$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...
3137
			$pos += strlen($code) - 1 + 2;
3138
		}
3139
3140
		// If this is block level, eat any breaks after it.
3141
		if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '<br>')
3142
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 5);
3143
3144
		// Are we trimming outside this tag?
3145
		if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
3146
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
3147
	}
3148
3149
	// Close any remaining tags.
3150
	while ($tag = array_pop($open_tags))
3151
		$message .= "\n" . $tag['after'] . "\n";
3152
3153
	// Parse the smileys within the parts where it can be done safely.
3154
	if ($smileys === true)
3155
	{
3156
		$message_parts = explode("\n", $message);
3157
		for ($i = 0, $n = count($message_parts); $i < $n; $i += 2)
3158
			parsesmileys($message_parts[$i]);
3159
3160
		$message = implode('', $message_parts);
3161
	}
3162
3163
	// No smileys, just get rid of the markers.
3164
	else
3165
		$message = strtr($message, array("\n" => ''));
3166
3167
	if ($message !== '' && $message[0] === ' ')
3168
		$message = '&nbsp;' . substr($message, 1);
3169
3170
	// Cleanup whitespace.
3171
	$message = strtr($message, array('  ' => ' &nbsp;', "\r" => '', "\n" => '<br>', '<br> ' => '<br>&nbsp;', '&#13;' => "\n"));
3172
3173
	// Allow mods access to what parse_bbc created
3174
	call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
3175
3176
	// Cache the output if it took some time...
3177
	if (isset($cache_key, $cache_t) && microtime(true) - $cache_t > 0.05)
3178
		cache_put_data($cache_key, $message, 240);
3179
3180
	// If this was a force parse revert if needed.
3181
	if (!empty($parse_tags))
3182
	{
3183
		$alltags_regex = empty($real_alltags_regex) ? '' : $real_alltags_regex;
3184
		unset($real_alltags_regex);
3185
	}
3186
	elseif (!empty($bbc_codes))
3187
		$bbc_lang_locales[$txt['lang_locale']] = $bbc_codes;
3188
3189
	return $message;
3190
}
3191
3192
/**
3193
 * Parse smileys in the passed message.
3194
 *
3195
 * The smiley parsing function which makes pretty faces appear :).
3196
 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
3197
 * These are specifically not parsed in code tags [url=mailto:[email protected]]
3198
 * Caches the smileys from the database or array in memory.
3199
 * Doesn't return anything, but rather modifies message directly.
3200
 *
3201
 * @param string &$message The message to parse smileys in
3202
 */
3203
function parsesmileys(&$message)
3204
{
3205
	global $modSettings, $txt, $user_info, $context, $smcFunc;
3206
	static $smileyPregSearch = null, $smileyPregReplacements = array();
3207
3208
	// No smiley set at all?!
3209
	if ($user_info['smiley_set'] == 'none' || trim($message) == '')
3210
		return;
3211
3212
	// Maybe a mod wants to implement an alternative method (e.g. emojis instead of images)
3213
	call_integration_hook('integrate_smileys', array(&$smileyPregSearch, &$smileyPregReplacements));
3214
3215
	// If smileyPregSearch hasn't been set, do it now.
3216
	if (empty($smileyPregSearch))
3217
	{
3218
		// Cache for longer when customized smiley codes aren't enabled
3219
		$cache_time = empty($modSettings['smiley_enable']) ? 7200 : 480;
3220
3221
		// Load the smileys in reverse order by length so they don't get parsed incorrectly.
3222
		if (($temp = cache_get_data('parsing_smileys_' . $user_info['smiley_set'], $cache_time)) == null)
3223
		{
3224
			$result = $smcFunc['db_query']('', '
3225
				SELECT s.code, f.filename, s.description
3226
				FROM {db_prefix}smileys AS s
3227
					JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
3228
				WHERE f.smiley_set = {string:smiley_set}' . (empty($modSettings['smiley_enable']) ? '
3229
					AND s.code IN ({array_string:default_codes})' : '') . '
3230
				ORDER BY LENGTH(s.code) DESC',
3231
				array(
3232
					'default_codes' => array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'),
3233
					'smiley_set' => $user_info['smiley_set'],
3234
				)
3235
			);
3236
			$smileysfrom = array();
3237
			$smileysto = array();
3238
			$smileysdescs = array();
3239
			while ($row = $smcFunc['db_fetch_assoc']($result))
3240
			{
3241
				$smileysfrom[] = $row['code'];
3242
				$smileysto[] = $smcFunc['htmlspecialchars']($row['filename']);
3243
				$smileysdescs[] = !empty($txt['icon_' . strtolower($row['description'])]) ? $txt['icon_' . strtolower($row['description'])] : $row['description'];
3244
			}
3245
			$smcFunc['db_free_result']($result);
3246
3247
			cache_put_data('parsing_smileys_' . $user_info['smiley_set'], array($smileysfrom, $smileysto, $smileysdescs), $cache_time);
3248
		}
3249
		else
3250
			list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
3251
3252
		// The non-breaking-space is a complex thing...
3253
		$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
3254
3255
		// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:[email protected]] doesn't parse the :D smiley)
3256
		$smileyPregReplacements = array();
3257
		$searchParts = array();
3258
		$smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
3259
3260
		for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
3261
		{
3262
			$specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES);
3263
			$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">';
3264
3265
			$smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
3266
3267
			$searchParts[] = $smileysfrom[$i];
3268
			if ($smileysfrom[$i] != $specialChars)
3269
			{
3270
				$smileyPregReplacements[$specialChars] = $smileyCode;
3271
				$searchParts[] = $specialChars;
3272
3273
				// Some 2.0 hex htmlchars are in there as 3 digits; allow for finding leading 0 or not
3274
				$specialChars2 = preg_replace('/&#(\d{2});/', '&#0$1;', $specialChars);
3275
				if ($specialChars2 != $specialChars)
3276
				{
3277
					$smileyPregReplacements[$specialChars2] = $smileyCode;
3278
					$searchParts[] = $specialChars2;
3279
				}
3280
			}
3281
		}
3282
3283
		$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

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

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

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

3576
					$image = @imagecreatefromstring(/** @scrutinizer ignore-type */ fetch_web_data($url));
Loading history...
3577
					if ($image !== false)
3578
					{
3579
						$size = array(imagesx($image), imagesy($image));
3580
						imagedestroy($image);
3581
					}
3582
				}
3583
			}
3584
		}
3585
	}
3586
3587
	// If we didn't get it, we failed.
3588
	if (!isset($size))
3589
		$size = false;
3590
3591
	// If this took a long time, we may never have to do it again, but then again we might...
3592
	if (microtime(true) - $t > 0.8)
3593
		cache_put_data('url_image_size-' . md5($url), $size, 240);
3594
3595
	// Didn't work.
3596
	return $size;
3597
}
3598
3599
/**
3600
 * Sets up the basic theme context stuff.
3601
 *
3602
 * @param bool $forceload Whether to load the theme even if it's already loaded
3603
 */
3604
function setupThemeContext($forceload = false)
3605
{
3606
	global $modSettings, $user_info, $scripturl, $context, $settings, $options, $txt, $maintenance;
3607
	global $smcFunc;
3608
	static $loaded = false;
3609
3610
	// Under SSI this function can be called more then once.  That can cause some problems.
3611
	//   So only run the function once unless we are forced to run it again.
3612
	if ($loaded && !$forceload)
3613
		return;
3614
3615
	$loaded = true;
3616
3617
	$context['in_maintenance'] = !empty($maintenance);
3618
	$context['current_time'] = timeformat(time(), false);
3619
	$context['current_action'] = isset($_GET['action']) ? $smcFunc['htmlspecialchars']($_GET['action']) : '';
3620
	$context['random_news_line'] = array();
3621
3622
	// Get some news...
3623
	$context['news_lines'] = array_filter(explode("\n", str_replace("\r", '', trim(addslashes($modSettings['news'])))));
3624
	for ($i = 0, $n = count($context['news_lines']); $i < $n; $i++)
3625
	{
3626
		if (trim($context['news_lines'][$i]) == '')
3627
			continue;
3628
3629
		// Clean it up for presentation ;).
3630
		$context['news_lines'][$i] = parse_bbc(stripslashes(trim($context['news_lines'][$i])), true, 'news' . $i);
3631
	}
3632
3633
	if (!empty($context['news_lines']) && (!empty($modSettings['allow_guestAccess']) || $context['user']['is_logged']))
3634
		$context['random_news_line'] = $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)];
3635
3636
	if (!$user_info['is_guest'])
3637
	{
3638
		$context['user']['messages'] = &$user_info['messages'];
3639
		$context['user']['unread_messages'] = &$user_info['unread_messages'];
3640
		$context['user']['alerts'] = &$user_info['alerts'];
3641
3642
		// Personal message popup...
3643
		if ($user_info['unread_messages'] > (isset($_SESSION['unread_messages']) ? $_SESSION['unread_messages'] : 0))
3644
			$context['user']['popup_messages'] = true;
3645
		else
3646
			$context['user']['popup_messages'] = false;
3647
		$_SESSION['unread_messages'] = $user_info['unread_messages'];
3648
3649
		if (allowedTo('moderate_forum'))
3650
			$context['unapproved_members'] = !empty($modSettings['unapprovedMembers']) ? $modSettings['unapprovedMembers'] : 0;
3651
3652
		$context['user']['avatar'] = array();
3653
3654
		// Check for gravatar first since we might be forcing them...
3655
		if (!empty($modSettings['gravatarEnabled']) && (substr($user_info['avatar']['url'], 0, 11) == 'gravatar://' || !empty($modSettings['gravatarOverride'])))
3656
		{
3657
			if (!empty($modSettings['gravatarAllowExtraEmail']) && stristr($user_info['avatar']['url'], 'gravatar://') && strlen($user_info['avatar']['url']) > 11)
3658
				$context['user']['avatar']['href'] = get_gravatar_url($smcFunc['substr']($user_info['avatar']['url'], 11));
3659
			else
3660
				$context['user']['avatar']['href'] = get_gravatar_url($user_info['email']);
3661
		}
3662
		// Uploaded?
3663
		elseif ($user_info['avatar']['url'] == '' && !empty($user_info['avatar']['id_attach']))
3664
			$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';
3665
		// Full URL?
3666
		elseif (strpos($user_info['avatar']['url'], 'http://') === 0 || strpos($user_info['avatar']['url'], 'https://') === 0)
3667
			$context['user']['avatar']['href'] = $user_info['avatar']['url'];
3668
		// Otherwise we assume it's server stored.
3669
		elseif ($user_info['avatar']['url'] != '')
3670
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/' . $smcFunc['htmlspecialchars']($user_info['avatar']['url']);
3671
		// No avatar at all? Fine, we have a big fat default avatar ;)
3672
		else
3673
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/default.png';
3674
3675
		if (!empty($context['user']['avatar']))
3676
			$context['user']['avatar']['image'] = '<img src="' . $context['user']['avatar']['href'] . '" alt="" class="avatar">';
3677
3678
		// Figure out how long they've been logged in.
3679
		$context['user']['total_time_logged_in'] = array(
3680
			'days' => floor($user_info['total_time_logged_in'] / 86400),
3681
			'hours' => floor(($user_info['total_time_logged_in'] % 86400) / 3600),
3682
			'minutes' => floor(($user_info['total_time_logged_in'] % 3600) / 60)
3683
		);
3684
	}
3685
	else
3686
	{
3687
		$context['user']['messages'] = 0;
3688
		$context['user']['unread_messages'] = 0;
3689
		$context['user']['avatar'] = array();
3690
		$context['user']['total_time_logged_in'] = array('days' => 0, 'hours' => 0, 'minutes' => 0);
3691
		$context['user']['popup_messages'] = false;
3692
3693
		// If we've upgraded recently, go easy on the passwords.
3694
		if (!empty($modSettings['disableHashTime']) && ($modSettings['disableHashTime'] == 1 || time() < $modSettings['disableHashTime']))
3695
			$context['disable_login_hashing'] = true;
3696
	}
3697
3698
	// Setup the main menu items.
3699
	setupMenuContext();
3700
3701
	// This is here because old index templates might still use it.
3702
	$context['show_news'] = !empty($settings['enable_news']);
3703
3704
	// This is done to allow theme authors to customize it as they want.
3705
	$context['show_pm_popup'] = $context['user']['popup_messages'] && !empty($options['popup_messages']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != 'pm');
3706
3707
	// 2.1+: Add the PM popup here instead. Theme authors can still override it simply by editing/removing the 'fPmPopup' in the array.
3708
	if ($context['show_pm_popup'])
3709
		addInlineJavaScript('
3710
		jQuery(document).ready(function($) {
3711
			new smc_Popup({
3712
				heading: ' . JavaScriptEscape($txt['show_personal_messages_heading']) . ',
3713
				content: ' . JavaScriptEscape(sprintf($txt['show_personal_messages'], $context['user']['unread_messages'], $scripturl . '?action=pm')) . ',
3714
				icon_class: \'main_icons mail_new\'
3715
			});
3716
		});');
3717
3718
	// Add a generic "Are you sure?" confirmation message.
3719
	addInlineJavaScript('
3720
	var smf_you_sure =' . JavaScriptEscape($txt['quickmod_confirm']) . ';');
3721
3722
	// Now add the capping code for avatars.
3723
	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')
3724
		addInlineCss('
3725
	img.avatar { max-width: ' . $modSettings['avatar_max_width_external'] . 'px; max-height: ' . $modSettings['avatar_max_height_external'] . 'px; }');
3726
3727
	// Add max image limits
3728
	if (!empty($modSettings['max_image_width']))
3729
		addInlineCss('
3730
	.postarea .bbc_img { max-width: ' . $modSettings['max_image_width'] . 'px; }');
3731
3732
	if (!empty($modSettings['max_image_height']))
3733
		addInlineCss('
3734
	.postarea .bbc_img { max-height: ' . $modSettings['max_image_height'] . 'px; }');
3735
3736
	// This looks weird, but it's because BoardIndex.php references the variable.
3737
	$context['common_stats']['latest_member'] = array(
3738
		'id' => $modSettings['latestMember'],
3739
		'name' => $modSettings['latestRealName'],
3740
		'href' => $scripturl . '?action=profile;u=' . $modSettings['latestMember'],
3741
		'link' => '<a href="' . $scripturl . '?action=profile;u=' . $modSettings['latestMember'] . '">' . $modSettings['latestRealName'] . '</a>',
3742
	);
3743
	$context['common_stats'] = array(
3744
		'total_posts' => comma_format($modSettings['totalMessages']),
3745
		'total_topics' => comma_format($modSettings['totalTopics']),
3746
		'total_members' => comma_format($modSettings['totalMembers']),
3747
		'latest_member' => $context['common_stats']['latest_member'],
3748
	);
3749
	$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']);
3750
3751
	if (empty($settings['theme_version']))
3752
		addJavaScriptVar('smf_scripturl', $scripturl);
3753
3754
	if (!isset($context['page_title']))
3755
		$context['page_title'] = '';
3756
3757
	// Set some specific vars.
3758
	$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3759
	$context['meta_keywords'] = !empty($modSettings['meta_keywords']) ? $smcFunc['htmlspecialchars']($modSettings['meta_keywords']) : '';
3760
3761
	// Content related meta tags, including Open Graph
3762
	$context['meta_tags'][] = array('property' => 'og:site_name', 'content' => $context['forum_name']);
3763
	$context['meta_tags'][] = array('property' => 'og:title', 'content' => $context['page_title_html_safe']);
3764
3765
	if (!empty($context['meta_keywords']))
3766
		$context['meta_tags'][] = array('name' => 'keywords', 'content' => $context['meta_keywords']);
3767
3768
	if (!empty($context['canonical_url']))
3769
		$context['meta_tags'][] = array('property' => 'og:url', 'content' => $context['canonical_url']);
3770
3771
	if (!empty($settings['og_image']))
3772
		$context['meta_tags'][] = array('property' => 'og:image', 'content' => $settings['og_image']);
3773
3774
	if (!empty($context['meta_description']))
3775
	{
3776
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['meta_description']);
3777
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['meta_description']);
3778
	}
3779
	else
3780
	{
3781
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['page_title_html_safe']);
3782
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['page_title_html_safe']);
3783
	}
3784
3785
	call_integration_hook('integrate_theme_context');
3786
}
3787
3788
/**
3789
 * Helper function to set the system memory to a needed value
3790
 * - If the needed memory is greater than current, will attempt to get more
3791
 * - if in_use is set to true, will also try to take the current memory usage in to account
3792
 *
3793
 * @param string $needed The amount of memory to request, if needed, like 256M
3794
 * @param bool $in_use Set to true to account for current memory usage of the script
3795
 * @return boolean True if we have at least the needed memory
3796
 */
3797
function setMemoryLimit($needed, $in_use = false)
3798
{
3799
	// everything in bytes
3800
	$memory_current = memoryReturnBytes(ini_get('memory_limit'));
3801
	$memory_needed = memoryReturnBytes($needed);
3802
3803
	// should we account for how much is currently being used?
3804
	if ($in_use)
3805
		$memory_needed += function_exists('memory_get_usage') ? memory_get_usage() : (2 * 1048576);
3806
3807
	// if more is needed, request it
3808
	if ($memory_current < $memory_needed)
3809
	{
3810
		@ini_set('memory_limit', ceil($memory_needed / 1048576) . 'M');
3811
		$memory_current = memoryReturnBytes(ini_get('memory_limit'));
3812
	}
3813
3814
	$memory_current = max($memory_current, memoryReturnBytes(get_cfg_var('memory_limit')));
0 ignored issues
show
Bug introduced by
It seems like get_cfg_var('memory_limit') can also be of type array; however, parameter $val of memoryReturnBytes() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

3814
	$memory_current = max($memory_current, memoryReturnBytes(/** @scrutinizer ignore-type */ get_cfg_var('memory_limit')));
Loading history...
3815
3816
	// return success or not
3817
	return (bool) ($memory_current >= $memory_needed);
3818
}
3819
3820
/**
3821
 * Helper function to convert memory string settings to bytes
3822
 *
3823
 * @param string $val The byte string, like 256M or 1G
3824
 * @return integer The string converted to a proper integer in bytes
3825
 */
3826
function memoryReturnBytes($val)
3827
{
3828
	if (is_integer($val))
0 ignored issues
show
introduced by
The condition is_integer($val) is always false.
Loading history...
3829
		return $val;
3830
3831
	// Separate the number from the designator
3832
	$val = trim($val);
3833
	$num = intval(substr($val, 0, strlen($val) - 1));
3834
	$last = strtolower(substr($val, -1));
3835
3836
	// convert to bytes
3837
	switch ($last)
3838
	{
3839
		case 'g':
3840
			$num *= 1024;
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
3841
		case 'm':
3842
			$num *= 1024;
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
3843
		case 'k':
3844
			$num *= 1024;
3845
	}
3846
	return $num;
3847
}
3848
3849
/**
3850
 * The header template
3851
 */
3852
function template_header()
3853
{
3854
	global $txt, $modSettings, $context, $user_info, $boarddir, $cachedir, $cache_enable, $language;
3855
3856
	setupThemeContext();
3857
3858
	// Print stuff to prevent caching of pages (except on attachment errors, etc.)
3859
	if (empty($context['no_last_modified']))
3860
	{
3861
		header('expires: Mon, 26 Jul 1997 05:00:00 GMT');
3862
		header('last-modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
3863
3864
		// Are we debugging the template/html content?
3865
		if (!isset($_REQUEST['xml']) && isset($_GET['debug']) && !isBrowser('ie'))
3866
			header('content-type: application/xhtml+xml');
3867
		elseif (!isset($_REQUEST['xml']))
3868
			header('content-type: text/html; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
3869
	}
3870
3871
	header('content-type: text/' . (isset($_REQUEST['xml']) ? 'xml' : 'html') . '; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
3872
3873
	// We need to splice this in after the body layer, or after the main layer for older stuff.
3874
	if ($context['in_maintenance'] && $context['user']['is_admin'])
3875
	{
3876
		$position = array_search('body', $context['template_layers']);
3877
		if ($position === false)
3878
			$position = array_search('main', $context['template_layers']);
3879
3880
		if ($position !== false)
3881
		{
3882
			$before = array_slice($context['template_layers'], 0, $position + 1);
3883
			$after = array_slice($context['template_layers'], $position + 1);
3884
			$context['template_layers'] = array_merge($before, array('maint_warning'), $after);
3885
		}
3886
	}
3887
3888
	$checked_securityFiles = false;
3889
	$showed_banned = false;
3890
	foreach ($context['template_layers'] as $layer)
3891
	{
3892
		loadSubTemplate($layer . '_above', true);
3893
3894
		// May seem contrived, but this is done in case the body and main layer aren't there...
3895
		if (in_array($layer, array('body', 'main')) && allowedTo('admin_forum') && !$user_info['is_guest'] && !$checked_securityFiles)
3896
		{
3897
			$checked_securityFiles = true;
3898
3899
			$securityFiles = array('install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~');
3900
3901
			// Add your own files.
3902
			call_integration_hook('integrate_security_files', array(&$securityFiles));
3903
3904
			foreach ($securityFiles as $i => $securityFile)
3905
			{
3906
				if (!file_exists($boarddir . '/' . $securityFile))
3907
					unset($securityFiles[$i]);
3908
			}
3909
3910
			// We are already checking so many files...just few more doesn't make any difference! :P
3911
			if (!empty($modSettings['currentAttachmentUploadDir']))
3912
				$path = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
3913
3914
			else
3915
				$path = $modSettings['attachmentUploadDir'];
3916
3917
			secureDirectory($path, true);
3918
			secureDirectory($cachedir);
3919
3920
			// If agreement is enabled, at least the english version shall exist
3921
			if (!empty($modSettings['requireAgreement']))
3922
				$agreement = !file_exists($boarddir . '/agreement.txt');
3923
3924
			// If privacy policy is enabled, at least the default language version shall exist
3925
			if (!empty($modSettings['requirePolicyAgreement']))
3926
				$policy_agreement = empty($modSettings['policy_' . $language]);
3927
3928
			if (!empty($securityFiles) ||
3929
				(!empty($cache_enable) && !is_writable($cachedir)) ||
3930
				!empty($agreement) ||
3931
				!empty($policy_agreement) ||
3932
				!empty($context['auth_secret_missing']))
3933
			{
3934
				echo '
3935
		<div class="errorbox">
3936
			<p class="alert">!!</p>
3937
			<h3>', empty($securityFiles) && empty($context['auth_secret_missing']) ? $txt['generic_warning'] : $txt['security_risk'], '</h3>
3938
			<p>';
3939
3940
				foreach ($securityFiles as $securityFile)
3941
				{
3942
					echo '
3943
				', $txt['not_removed'], '<strong>', $securityFile, '</strong>!<br>';
3944
3945
					if ($securityFile == 'Settings.php~' || $securityFile == 'Settings_bak.php~')
3946
						echo '
3947
				', sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)), '<br>';
3948
				}
3949
3950
				if (!empty($cache_enable) && !is_writable($cachedir))
3951
					echo '
3952
				<strong>', $txt['cache_writable'], '</strong><br>';
3953
3954
				if (!empty($agreement))
3955
					echo '
3956
				<strong>', $txt['agreement_missing'], '</strong><br>';
3957
3958
				if (!empty($policy_agreement))
3959
					echo '
3960
				<strong>', $txt['policy_agreement_missing'], '</strong><br>';
3961
3962
				if (!empty($context['auth_secret_missing']))
3963
					echo '
3964
				<strong>', $txt['auth_secret_missing'], '</strong><br>';
3965
3966
				echo '
3967
			</p>
3968
		</div>';
3969
			}
3970
		}
3971
		// If the user is banned from posting inform them of it.
3972
		elseif (in_array($layer, array('main', 'body')) && isset($_SESSION['ban']['cannot_post']) && !$showed_banned)
3973
		{
3974
			$showed_banned = true;
3975
			echo '
3976
				<div class="windowbg alert" style="margin: 2ex; padding: 2ex; border: 2px dashed red;">
3977
					', sprintf($txt['you_are_post_banned'], $user_info['is_guest'] ? $txt['guest_title'] : $user_info['name']);
3978
3979
			if (!empty($_SESSION['ban']['cannot_post']['reason']))
3980
				echo '
3981
					<div style="padding-left: 4ex; padding-top: 1ex;">', $_SESSION['ban']['cannot_post']['reason'], '</div>';
3982
3983
			if (!empty($_SESSION['ban']['expire_time']))
3984
				echo '
3985
					<div>', sprintf($txt['your_ban_expires'], timeformat($_SESSION['ban']['expire_time'], false)), '</div>';
3986
			else
3987
				echo '
3988
					<div>', $txt['your_ban_expires_never'], '</div>';
3989
3990
			echo '
3991
				</div>';
3992
		}
3993
	}
3994
}
3995
3996
/**
3997
 * Show the copyright.
3998
 */
3999
function theme_copyright()
4000
{
4001
	global $forum_copyright, $scripturl;
4002
4003
	// Don't display copyright for things like SSI.
4004
	if (SMF !== 1)
0 ignored issues
show
introduced by
The condition SMF !== 1 is always true.
Loading history...
4005
		return;
4006
4007
	// Put in the version...
4008
	printf($forum_copyright, SMF_FULL_VERSION, SMF_SOFTWARE_YEAR, $scripturl);
4009
}
4010
4011
/**
4012
 * The template footer
4013
 */
4014
function template_footer()
4015
{
4016
	global $context, $modSettings, $db_count;
4017
4018
	// Show the load time?  (only makes sense for the footer.)
4019
	$context['show_load_time'] = !empty($modSettings['timeLoadPageEnable']);
4020
	$context['load_time'] = round(microtime(true) - TIME_START, 3);
4021
	$context['load_queries'] = $db_count;
4022
4023
	if (!empty($context['template_layers']) && is_array($context['template_layers']))
4024
		foreach (array_reverse($context['template_layers']) as $layer)
4025
			loadSubTemplate($layer . '_below', true);
4026
}
4027
4028
/**
4029
 * Output the Javascript files
4030
 * 	- tabbing in this function is to make the HTML source look good and proper
4031
 *  - if deferred is set function will output all JS set to load at page end
4032
 *
4033
 * @param bool $do_deferred If true will only output the deferred JS (the stuff that goes right before the closing body tag)
4034
 */
4035
function template_javascript($do_deferred = false)
4036
{
4037
	global $context, $modSettings, $settings;
4038
4039
	// Use this hook to minify/optimize Javascript files and vars
4040
	call_integration_hook('integrate_pre_javascript_output', array(&$do_deferred));
4041
4042
	$toMinify = array(
4043
		'standard' => array(),
4044
		'defer' => array(),
4045
		'async' => array(),
4046
	);
4047
4048
	// Ouput the declared Javascript variables.
4049
	if (!empty($context['javascript_vars']) && !$do_deferred)
4050
	{
4051
		echo '
4052
	<script>';
4053
4054
		foreach ($context['javascript_vars'] as $key => $value)
4055
		{
4056
			if (empty($value))
4057
			{
4058
				echo '
4059
		var ', $key, ';';
4060
			}
4061
			else
4062
			{
4063
				echo '
4064
		var ', $key, ' = ', $value, ';';
4065
			}
4066
		}
4067
4068
		echo '
4069
	</script>';
4070
	}
4071
4072
	// In the dark days before HTML5, deferred JS files needed to be loaded at the end of the body.
4073
	// Now we load them in the head and use 'async' and/or 'defer' attributes. Much better performance.
4074
	if (!$do_deferred)
4075
	{
4076
		// While we have JavaScript files to place in the template.
4077
		foreach ($context['javascript_files'] as $id => $js_file)
4078
		{
4079
			// Last minute call! allow theme authors to disable single files.
4080
			if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
4081
				continue;
4082
4083
			// By default files don't get minimized unless the file explicitly says so!
4084
			if (!empty($js_file['options']['minimize']) && !empty($modSettings['minimize_files']))
4085
			{
4086
				if (!empty($js_file['options']['async']))
4087
					$toMinify['async'][] = $js_file;
4088
4089
				elseif (!empty($js_file['options']['defer']))
4090
					$toMinify['defer'][] = $js_file;
4091
4092
				else
4093
					$toMinify['standard'][] = $js_file;
4094
4095
				// Grab a random seed.
4096
				if (!isset($minSeed) && isset($js_file['options']['seed']))
4097
					$minSeed = $js_file['options']['seed'];
4098
			}
4099
4100
			else
4101
			{
4102
				echo '
4103
	<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' : '';
4104
4105
				if (!empty($js_file['options']['attributes']))
4106
					foreach ($js_file['options']['attributes'] as $key => $value)
4107
					{
4108
						if (is_bool($value))
4109
							echo !empty($value) ? ' ' . $key : '';
4110
4111
						else
4112
							echo ' ', $key, '="', $value, '"';
4113
					}
4114
4115
				echo '></script>';
4116
			}
4117
		}
4118
4119
		foreach ($toMinify as $js_files)
4120
		{
4121
			if (!empty($js_files))
4122
			{
4123
				$result = custMinify($js_files, 'js');
4124
4125
				$minSuccessful = array_keys($result) === array('smf_minified');
4126
4127
				foreach ($result as $minFile)
4128
					echo '
4129
	<script src="', $minFile['fileUrl'], $minSuccessful && isset($minSeed) ? $minSeed : '', '"', !empty($minFile['options']['async']) ? ' async' : '', !empty($minFile['options']['defer']) ? ' defer' : '', '></script>';
4130
			}
4131
		}
4132
	}
4133
4134
	// Inline JavaScript - Actually useful some times!
4135
	if (!empty($context['javascript_inline']))
4136
	{
4137
		if (!empty($context['javascript_inline']['defer']) && $do_deferred)
4138
		{
4139
			echo '
4140
<script>
4141
window.addEventListener("DOMContentLoaded", function() {';
4142
4143
			foreach ($context['javascript_inline']['defer'] as $js_code)
4144
				echo $js_code;
4145
4146
			echo '
4147
});
4148
</script>';
4149
		}
4150
4151
		if (!empty($context['javascript_inline']['standard']) && !$do_deferred)
4152
		{
4153
			echo '
4154
	<script>';
4155
4156
			foreach ($context['javascript_inline']['standard'] as $js_code)
4157
				echo $js_code;
4158
4159
			echo '
4160
	</script>';
4161
		}
4162
	}
4163
}
4164
4165
/**
4166
 * Output the CSS files
4167
 */
4168
function template_css()
4169
{
4170
	global $context, $db_show_debug, $boardurl, $settings, $modSettings;
4171
4172
	// Use this hook to minify/optimize CSS files
4173
	call_integration_hook('integrate_pre_css_output');
4174
4175
	$toMinify = array();
4176
	$normal = array();
4177
4178
	uasort($context['css_files'], function ($a, $b)
4179
	{
4180
		return $a['options']['order_pos'] < $b['options']['order_pos'] ? -1 : ($a['options']['order_pos'] > $b['options']['order_pos'] ? 1 : 0);
4181
	});
4182
	foreach ($context['css_files'] as $id => $file)
4183
	{
4184
		// Last minute call! allow theme authors to disable single files.
4185
		if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
4186
			continue;
4187
4188
		// Files are minimized unless they explicitly opt out.
4189
		if (!isset($file['options']['minimize']))
4190
			$file['options']['minimize'] = true;
4191
4192
		if (!empty($file['options']['minimize']) && !empty($modSettings['minimize_files']) && !isset($_REQUEST['normalcss']))
4193
		{
4194
			$toMinify[] = $file;
4195
4196
			// Grab a random seed.
4197
			if (!isset($minSeed) && isset($file['options']['seed']))
4198
				$minSeed = $file['options']['seed'];
4199
		}
4200
		else
4201
			$normal[] = array(
4202
				'url' => $file['fileUrl'] . (isset($file['options']['seed']) ? $file['options']['seed'] : ''),
4203
				'attributes' => !empty($file['options']['attributes']) ? $file['options']['attributes'] : array()
4204
			);
4205
	}
4206
4207
	if (!empty($toMinify))
4208
	{
4209
		$result = custMinify($toMinify, 'css');
4210
4211
		$minSuccessful = array_keys($result) === array('smf_minified');
4212
4213
		foreach ($result as $minFile)
4214
			echo '
4215
	<link rel="stylesheet" href="', $minFile['fileUrl'], $minSuccessful && isset($minSeed) ? $minSeed : '', '">';
4216
	}
4217
4218
	// Print the rest after the minified files.
4219
	if (!empty($normal))
4220
		foreach ($normal as $nf)
4221
		{
4222
			echo '
4223
	<link rel="stylesheet" href="', $nf['url'], '"';
4224
4225
			if (!empty($nf['attributes']))
4226
				foreach ($nf['attributes'] as $key => $value)
4227
				{
4228
					if (is_bool($value))
4229
						echo !empty($value) ? ' ' . $key : '';
4230
					else
4231
						echo ' ', $key, '="', $value, '"';
4232
				}
4233
4234
			echo '>';
4235
		}
4236
4237
	if ($db_show_debug === true)
4238
	{
4239
		// Try to keep only what's useful.
4240
		$repl = array($boardurl . '/Themes/' => '', $boardurl . '/' => '');
4241
		foreach ($context['css_files'] as $file)
4242
			$context['debug']['sheets'][] = strtr($file['fileName'], $repl);
4243
	}
4244
4245
	if (!empty($context['css_header']))
4246
	{
4247
		echo '
4248
	<style>';
4249
4250
		foreach ($context['css_header'] as $css)
4251
			echo $css . '
4252
	';
4253
4254
		echo '
4255
	</style>';
4256
	}
4257
}
4258
4259
/**
4260
 * Get an array of previously defined files and adds them to our main minified files.
4261
 * Sets a one day cache to avoid re-creating a file on every request.
4262
 *
4263
 * @param array $data The files to minify.
4264
 * @param string $type either css or js.
4265
 * @return array Info about the minified file, or about the original files if the minify process failed.
4266
 */
4267
function custMinify($data, $type)
4268
{
4269
	global $settings, $txt;
4270
4271
	$types = array('css', 'js');
4272
	$type = !empty($type) && in_array($type, $types) ? $type : false;
4273
	$data = is_array($data) ? $data : array();
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
4274
4275
	if (empty($type) || empty($data))
4276
		return $data;
4277
4278
	// Different pages include different files, so we use a hash to label the different combinations
4279
	$hash = md5(implode(' ', array_map(function($file)
4280
	{
4281
		return $file['filePath'] . '-' . $file['mtime'];
4282
	}, $data)));
4283
4284
	// Is this a deferred or asynchronous JavaScript file?
4285
	$async = $type === 'js';
4286
	$defer = $type === 'js';
4287
	if ($type === 'js')
4288
	{
4289
		foreach ($data as $id => $file)
4290
		{
4291
			// A minified script should only be loaded asynchronously if all its components wanted to be.
4292
			if (empty($file['options']['async']))
4293
				$async = false;
4294
4295
			// A minified script should only be deferred if all its components wanted to be.
4296
			if (empty($file['options']['defer']))
4297
				$defer = false;
4298
		}
4299
	}
4300
4301
	// Did we already do this?
4302
	$minified_file = $settings['theme_dir'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/minified_' . $hash . '.' . $type;
4303
	$already_exists = file_exists($minified_file);
4304
4305
	// Already done?
4306
	if ($already_exists)
4307
	{
4308
		return array('smf_minified' => array(
4309
			'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file),
4310
			'filePath' => $minified_file,
4311
			'fileName' => basename($minified_file),
4312
			'options' => array('async' => !empty($async), 'defer' => !empty($defer)),
4313
		));
4314
	}
4315
	// File has to exist. If it doesn't, try to create it.
4316
	elseif (@fopen($minified_file, 'w') === false || !smf_chmod($minified_file))
4317
	{
4318
		loadLanguage('Errors');
4319
		log_error(sprintf($txt['file_not_created'], $minified_file), 'general');
4320
4321
		// The process failed, so roll back to print each individual file.
4322
		return $data;
4323
	}
4324
4325
	// No namespaces, sorry!
4326
	$classType = 'MatthiasMullie\\Minify\\' . strtoupper($type);
4327
4328
	$minifier = new $classType();
4329
4330
	foreach ($data as $id => $file)
4331
	{
4332
		$toAdd = !empty($file['filePath']) && file_exists($file['filePath']) ? $file['filePath'] : false;
4333
4334
		// The file couldn't be located so it won't be added. Log this error.
4335
		if (empty($toAdd))
4336
		{
4337
			loadLanguage('Errors');
4338
			log_error(sprintf($txt['file_minimize_fail'], !empty($file['fileName']) ? $file['fileName'] : $id), 'general');
4339
			continue;
4340
		}
4341
4342
		// Add this file to the list.
4343
		$minifier->add($toAdd);
4344
	}
4345
4346
	// Create the file.
4347
	$minifier->minify($minified_file);
4348
	unset($minifier);
4349
	clearstatcache();
4350
4351
	// Minify process failed.
4352
	if (!filesize($minified_file))
4353
	{
4354
		loadLanguage('Errors');
4355
		log_error(sprintf($txt['file_not_created'], $minified_file), 'general');
4356
4357
		// The process failed so roll back to print each individual file.
4358
		return $data;
4359
	}
4360
4361
	return array('smf_minified' => array(
4362
		'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file),
4363
		'filePath' => $minified_file,
4364
		'fileName' => basename($minified_file),
4365
		'options' => array('async' => $async, 'defer' => $defer),
4366
	));
4367
}
4368
4369
/**
4370
 * Clears out old minimized CSS and JavaScript files and ensures $modSettings['browser_cache'] is up to date
4371
 */
4372
function deleteAllMinified()
4373
{
4374
	global $smcFunc, $txt, $modSettings;
4375
4376
	$not_deleted = array();
4377
	$most_recent = 0;
4378
4379
	// Kinda sucks that we need to do another query to get all the theme dirs, but c'est la vie.
4380
	$request = $smcFunc['db_query']('', '
4381
		SELECT id_theme AS id, value AS dir
4382
		FROM {db_prefix}themes
4383
		WHERE variable = {string:var}',
4384
		array(
4385
			'var' => 'theme_dir',
4386
		)
4387
	);
4388
	while ($theme = $smcFunc['db_fetch_assoc']($request))
4389
	{
4390
		foreach (array('css', 'js') as $type)
4391
		{
4392
			foreach (glob(rtrim($theme['dir'], '/') . '/' . ($type == 'css' ? 'css' : 'scripts') . '/*.' . $type) as $filename)
4393
			{
4394
				// We want to find the most recent mtime of non-minified files
4395
				if (strpos(pathinfo($filename, PATHINFO_BASENAME), 'minified') === false)
0 ignored issues
show
Bug introduced by
It seems like pathinfo($filename, PATHINFO_BASENAME) can also be of type array; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

4395
				if (strpos(/** @scrutinizer ignore-type */ pathinfo($filename, PATHINFO_BASENAME), 'minified') === false)
Loading history...
4396
					$most_recent = max($modSettings['browser_cache'], (int) @filemtime($filename));
4397
4398
				// Try to delete minified files. Add them to our error list if that fails.
4399
				elseif (!@unlink($filename))
4400
					$not_deleted[] = $filename;
4401
			}
4402
		}
4403
	}
4404
	$smcFunc['db_free_result']($request);
4405
4406
	// This setting tracks the most recent modification time of any of our CSS and JS files
4407
	if ($most_recent > $modSettings['browser_cache'])
4408
		updateSettings(array('browser_cache' => $most_recent));
4409
4410
	// If any of the files could not be deleted, log an error about it.
4411
	if (!empty($not_deleted))
4412
	{
4413
		loadLanguage('Errors');
4414
		log_error(sprintf($txt['unlink_minimized_fail'], implode('<br>', $not_deleted)), 'general');
4415
	}
4416
}
4417
4418
/**
4419
 * Get an attachment's encrypted filename. If $new is true, won't check for file existence.
4420
 *
4421
 * @todo this currently returns the hash if new, and the full filename otherwise.
4422
 * Something messy like that.
4423
 * @todo and of course everything relies on this behavior and work around it. :P.
4424
 * Converters included.
4425
 *
4426
 * @param string $filename The name of the file
4427
 * @param int $attachment_id The ID of the attachment
4428
 * @param string|null $dir Which directory it should be in (null to use current one)
4429
 * @param bool $new Whether this is a new attachment
4430
 * @param string $file_hash The file hash
4431
 * @return string The path to the file
4432
 */
4433
function getAttachmentFilename($filename, $attachment_id, $dir = null, $new = false, $file_hash = '')
4434
{
4435
	global $modSettings, $smcFunc;
4436
4437
	// Just make up a nice hash...
4438
	if ($new)
4439
		return sha1(md5($filename . time()) . mt_rand());
4440
4441
	// Just make sure that attachment id is only a int
4442
	$attachment_id = (int) $attachment_id;
4443
4444
	// Grab the file hash if it wasn't added.
4445
	// Left this for legacy.
4446
	if ($file_hash === '')
4447
	{
4448
		$request = $smcFunc['db_query']('', '
4449
			SELECT file_hash
4450
			FROM {db_prefix}attachments
4451
			WHERE id_attach = {int:id_attach}',
4452
			array(
4453
				'id_attach' => $attachment_id,
4454
			)
4455
		);
4456
4457
		if ($smcFunc['db_num_rows']($request) === 0)
4458
			return false;
4459
4460
		list ($file_hash) = $smcFunc['db_fetch_row']($request);
4461
		$smcFunc['db_free_result']($request);
4462
	}
4463
4464
	// Still no hash? mmm...
4465
	if (empty($file_hash))
4466
		$file_hash = sha1(md5($filename . time()) . mt_rand());
4467
4468
	// Are we using multiple directories?
4469
	if (is_array($modSettings['attachmentUploadDir']))
4470
		$path = $modSettings['attachmentUploadDir'][$dir];
4471
4472
	else
4473
		$path = $modSettings['attachmentUploadDir'];
4474
4475
	return $path . '/' . $attachment_id . '_' . $file_hash . '.dat';
4476
}
4477
4478
/**
4479
 * Convert a single IP to a ranged IP.
4480
 * internal function used to convert a user-readable format to a format suitable for the database.
4481
 *
4482
 * @param string $fullip The full IP
4483
 * @return array An array of IP parts
4484
 */
4485
function ip2range($fullip)
4486
{
4487
	// Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.)
4488
	if ($fullip == 'unknown')
4489
		$fullip = '255.255.255.255';
4490
4491
	$ip_parts = explode('-', $fullip);
4492
	$ip_array = array();
4493
4494
	// if ip 22.12.31.21
4495
	if (count($ip_parts) == 1 && isValidIP($fullip))
4496
	{
4497
		$ip_array['low'] = $fullip;
4498
		$ip_array['high'] = $fullip;
4499
		return $ip_array;
4500
	} // if ip 22.12.* -> 22.12.* - 22.12.*
4501
	elseif (count($ip_parts) == 1)
4502
	{
4503
		$ip_parts[0] = $fullip;
4504
		$ip_parts[1] = $fullip;
4505
	}
4506
4507
	// if ip 22.12.31.21-12.21.31.21
4508
	if (count($ip_parts) == 2 && isValidIP($ip_parts[0]) && isValidIP($ip_parts[1]))
4509
	{
4510
		$ip_array['low'] = $ip_parts[0];
4511
		$ip_array['high'] = $ip_parts[1];
4512
		return $ip_array;
4513
	}
4514
	elseif (count($ip_parts) == 2) // if ip 22.22.*-22.22.*
4515
	{
4516
		$valid_low = isValidIP($ip_parts[0]);
4517
		$valid_high = isValidIP($ip_parts[1]);
4518
		$count = 0;
4519
		$mode = (preg_match('/:/', $ip_parts[0]) > 0 ? ':' : '.');
4520
		$max = ($mode == ':' ? 'ffff' : '255');
4521
		$min = 0;
4522
		if (!$valid_low)
4523
		{
4524
			$ip_parts[0] = preg_replace('/\*/', '0', $ip_parts[0]);
4525
			$valid_low = isValidIP($ip_parts[0]);
4526
			while (!$valid_low)
4527
			{
4528
				$ip_parts[0] .= $mode . $min;
4529
				$valid_low = isValidIP($ip_parts[0]);
4530
				$count++;
4531
				if ($count > 9) break;
4532
			}
4533
		}
4534
4535
		$count = 0;
4536
		if (!$valid_high)
4537
		{
4538
			$ip_parts[1] = preg_replace('/\*/', $max, $ip_parts[1]);
4539
			$valid_high = isValidIP($ip_parts[1]);
4540
			while (!$valid_high)
4541
			{
4542
				$ip_parts[1] .= $mode . $max;
4543
				$valid_high = isValidIP($ip_parts[1]);
4544
				$count++;
4545
				if ($count > 9) break;
4546
			}
4547
		}
4548
4549
		if ($valid_high && $valid_low)
4550
		{
4551
			$ip_array['low'] = $ip_parts[0];
4552
			$ip_array['high'] = $ip_parts[1];
4553
		}
4554
	}
4555
4556
	return $ip_array;
4557
}
4558
4559
/**
4560
 * Lookup an IP; try shell_exec first because we can do a timeout on it.
4561
 *
4562
 * @param string $ip The IP to get the hostname from
4563
 * @return string The hostname
4564
 */
4565
function host_from_ip($ip)
4566
{
4567
	global $modSettings;
4568
4569
	if (($host = cache_get_data('hostlookup-' . $ip, 600)) !== null)
4570
		return $host;
4571
	$t = microtime(true);
4572
4573
	// Try the Linux host command, perhaps?
4574
	if (!isset($host) && (strpos(strtolower(PHP_OS), 'win') === false || strpos(strtolower(PHP_OS), 'darwin') !== false) && mt_rand(0, 1) == 1)
4575
	{
4576
		if (!isset($modSettings['host_to_dis']))
4577
			$test = @shell_exec('host -W 1 ' . @escapeshellarg($ip));
4578
		else
4579
			$test = @shell_exec('host ' . @escapeshellarg($ip));
4580
4581
		// Did host say it didn't find anything?
4582
		if (strpos($test, 'not found') !== false)
0 ignored issues
show
Bug introduced by
It seems like $test can also be of type false; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

4582
		if (strpos(/** @scrutinizer ignore-type */ $test, 'not found') !== false)
Loading history...
4583
			$host = '';
4584
		// Invalid server option?
4585
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
4586
			updateSettings(array('host_to_dis' => 1));
4587
		// Maybe it found something, after all?
4588
		elseif (preg_match('~\s([^\s]+?)\.\s~', $test, $match) == 1)
0 ignored issues
show
Bug introduced by
It seems like $test can also be of type false; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

4588
		elseif (preg_match('~\s([^\s]+?)\.\s~', /** @scrutinizer ignore-type */ $test, $match) == 1)
Loading history...
4589
			$host = $match[1];
4590
	}
4591
4592
	// This is nslookup; usually only Windows, but possibly some Unix?
4593
	if (!isset($host) && stripos(PHP_OS, 'win') !== false && strpos(strtolower(PHP_OS), 'darwin') === false && mt_rand(0, 1) == 1)
4594
	{
4595
		$test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
4596
		if (strpos($test, 'Non-existent domain') !== false)
4597
			$host = '';
4598
		elseif (preg_match('~Name:\s+([^\s]+)~', $test, $match) == 1)
4599
			$host = $match[1];
4600
	}
4601
4602
	// This is the last try :/.
4603
	if (!isset($host) || $host === false)
4604
		$host = @gethostbyaddr($ip);
4605
4606
	// It took a long time, so let's cache it!
4607
	if (microtime(true) - $t > 0.5)
4608
		cache_put_data('hostlookup-' . $ip, $host, 600);
4609
4610
	return $host;
4611
}
4612
4613
/**
4614
 * Chops a string into words and prepares them to be inserted into (or searched from) the database.
4615
 *
4616
 * @param string $text The text to split into words
4617
 * @param int $max_chars The maximum number of characters per word
4618
 * @param bool $encrypt Whether to encrypt the results
4619
 * @return array An array of ints or words depending on $encrypt
4620
 */
4621
function text2words($text, $max_chars = 20, $encrypt = false)
4622
{
4623
	global $smcFunc, $context;
4624
4625
	// Upgrader may be working on old DBs...
4626
	if (!isset($context['utf8']))
4627
		$context['utf8'] = false;
4628
4629
	// Step 1: Remove entities/things we don't consider words:
4630
	$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>' => ' ')));
4631
4632
	// Step 2: Entities we left to letters, where applicable, lowercase.
4633
	$words = un_htmlspecialchars($smcFunc['strtolower']($words));
4634
4635
	// Step 3: Ready to split apart and index!
4636
	$words = explode(' ', $words);
4637
4638
	if ($encrypt)
4639
	{
4640
		$possible_chars = array_flip(array_merge(range(46, 57), range(65, 90), range(97, 122)));
4641
		$returned_ints = array();
4642
		foreach ($words as $word)
4643
		{
4644
			if (($word = trim($word, '-_\'')) !== '')
4645
			{
4646
				$encrypted = substr(crypt($word, 'uk'), 2, $max_chars);
4647
				$total = 0;
4648
				for ($i = 0; $i < $max_chars; $i++)
4649
					$total += $possible_chars[ord($encrypted[$i])] * pow(63, $i);
4650
				$returned_ints[] = $max_chars == 4 ? min($total, 16777215) : $total;
4651
			}
4652
		}
4653
		return array_unique($returned_ints);
4654
	}
4655
	else
4656
	{
4657
		// Trim characters before and after and add slashes for database insertion.
4658
		$returned_words = array();
4659
		foreach ($words as $word)
4660
			if (($word = trim($word, '-_\'')) !== '')
4661
				$returned_words[] = $max_chars === null ? $word : substr($word, 0, $max_chars);
4662
4663
		// Filter out all words that occur more than once.
4664
		return array_unique($returned_words);
4665
	}
4666
}
4667
4668
/**
4669
 * Creates an image/text button
4670
 *
4671
 * @deprecated since 2.1
4672
 * @param string $name The name of the button (should be a main_icons class or the name of an image)
4673
 * @param string $alt The alt text
4674
 * @param string $label The $txt string to use as the label
4675
 * @param string $custom Custom text/html to add to the img tag (only when using an actual image)
4676
 * @param boolean $force_use Whether to force use of this when template_create_button is available
4677
 * @return string The HTML to display the button
4678
 */
4679
function create_button($name, $alt, $label = '', $custom = '', $force_use = false)
4680
{
4681
	global $settings, $txt;
4682
4683
	// Does the current loaded theme have this and we are not forcing the usage of this function?
4684
	if (function_exists('template_create_button') && !$force_use)
4685
		return template_create_button($name, $alt, $label = '', $custom = '');
4686
4687
	if (!$settings['use_image_buttons'])
4688
		return $txt[$alt];
4689
	elseif (!empty($settings['use_buttons']))
4690
		return '<span class="main_icons ' . $name . '" alt="' . $txt[$alt] . '"></span>' . ($label != '' ? '&nbsp;<strong>' . $txt[$label] . '</strong>' : '');
4691
	else
4692
		return '<img src="' . $settings['lang_images_url'] . '/' . $name . '" alt="' . $txt[$alt] . '" ' . $custom . '>';
4693
}
4694
4695
/**
4696
 * Sets up all of the top menu buttons
4697
 * Saves them in the cache if it is available and on
4698
 * Places the results in $context
4699
 */
4700
function setupMenuContext()
4701
{
4702
	global $context, $modSettings, $user_info, $txt, $scripturl, $sourcedir, $settings, $smcFunc, $cache_enable;
4703
4704
	// Set up the menu privileges.
4705
	$context['allow_search'] = !empty($modSettings['allow_guestAccess']) ? allowedTo('search_posts') : (!$user_info['is_guest'] && allowedTo('search_posts'));
4706
	$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'));
4707
4708
	$context['allow_memberlist'] = allowedTo('view_mlist');
4709
	$context['allow_calendar'] = allowedTo('calendar_view') && !empty($modSettings['cal_enabled']);
4710
	$context['allow_moderation_center'] = $context['user']['can_mod'];
4711
	$context['allow_pm'] = allowedTo('pm_read');
4712
4713
	$cacheTime = $modSettings['lastActive'] * 60;
4714
4715
	// Initial "can you post an event in the calendar" option - but this might have been set in the calendar already.
4716
	if (!isset($context['allow_calendar_event']))
4717
	{
4718
		$context['allow_calendar_event'] = $context['allow_calendar'] && allowedTo('calendar_post');
4719
4720
		// If you don't allow events not linked to posts and you're not an admin, we have more work to do...
4721
		if ($context['allow_calendar'] && $context['allow_calendar_event'] && empty($modSettings['cal_allow_unlinked']) && !$user_info['is_admin'])
4722
		{
4723
			$boards_can_post = boardsAllowedTo('post_new');
4724
			$context['allow_calendar_event'] &= !empty($boards_can_post);
4725
		}
4726
	}
4727
4728
	// There is some menu stuff we need to do if we're coming at this from a non-guest perspective.
4729
	if (!$context['user']['is_guest'])
4730
	{
4731
		addInlineJavaScript('
4732
	var user_menus = new smc_PopupMenu();
4733
	user_menus.add("profile", "' . $scripturl . '?action=profile;area=popup");
4734
	user_menus.add("alerts", "' . $scripturl . '?action=profile;area=alerts_popup;u=' . $context['user']['id'] . '");', true);
4735
		if ($context['allow_pm'])
4736
			addInlineJavaScript('
4737
	user_menus.add("pm", "' . $scripturl . '?action=pm;sa=popup");', true);
4738
4739
		if (!empty($modSettings['enable_ajax_alerts']))
4740
		{
4741
			require_once($sourcedir . '/Subs-Notify.php');
4742
4743
			$timeout = getNotifyPrefs($context['user']['id'], 'alert_timeout', true);
4744
			$timeout = empty($timeout) ? 10000 : $timeout[$context['user']['id']]['alert_timeout'] * 1000;
4745
4746
			addInlineJavaScript('
4747
	var new_alert_title = "' . $context['forum_name_html_safe'] . '";
4748
	var alert_timeout = ' . $timeout . ';');
4749
			loadJavaScriptFile('alerts.js', array('minimize' => true), 'smf_alerts');
4750
		}
4751
	}
4752
4753
	// All the buttons we can possible want and then some, try pulling the final list of buttons from cache first.
4754
	if (($menu_buttons = cache_get_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $cacheTime)) === null || time() - $cacheTime <= $modSettings['settings_updated'])
4755
	{
4756
		$buttons = array(
4757
			'home' => array(
4758
				'title' => $txt['home'],
4759
				'href' => $scripturl,
4760
				'show' => true,
4761
				'sub_buttons' => array(
4762
				),
4763
				'is_last' => $context['right_to_left'],
4764
			),
4765
			'search' => array(
4766
				'title' => $txt['search'],
4767
				'href' => $scripturl . '?action=search',
4768
				'show' => $context['allow_search'],
4769
				'sub_buttons' => array(
4770
				),
4771
			),
4772
			'admin' => array(
4773
				'title' => $txt['admin'],
4774
				'href' => $scripturl . '?action=admin',
4775
				'show' => $context['allow_admin'],
4776
				'sub_buttons' => array(
4777
					'featuresettings' => array(
4778
						'title' => $txt['modSettings_title'],
4779
						'href' => $scripturl . '?action=admin;area=featuresettings',
4780
						'show' => allowedTo('admin_forum'),
4781
					),
4782
					'packages' => array(
4783
						'title' => $txt['package'],
4784
						'href' => $scripturl . '?action=admin;area=packages',
4785
						'show' => allowedTo('admin_forum'),
4786
					),
4787
					'errorlog' => array(
4788
						'title' => $txt['errorlog'],
4789
						'href' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc',
4790
						'show' => allowedTo('admin_forum') && !empty($modSettings['enableErrorLogging']),
4791
					),
4792
					'permissions' => array(
4793
						'title' => $txt['edit_permissions'],
4794
						'href' => $scripturl . '?action=admin;area=permissions',
4795
						'show' => allowedTo('manage_permissions'),
4796
					),
4797
					'memberapprove' => array(
4798
						'title' => $txt['approve_members_waiting'],
4799
						'href' => $scripturl . '?action=admin;area=viewmembers;sa=browse;type=approve',
4800
						'show' => !empty($context['unapproved_members']),
4801
						'is_last' => true,
4802
					),
4803
				),
4804
			),
4805
			'moderate' => array(
4806
				'title' => $txt['moderate'],
4807
				'href' => $scripturl . '?action=moderate',
4808
				'show' => $context['allow_moderation_center'],
4809
				'sub_buttons' => array(
4810
					'modlog' => array(
4811
						'title' => $txt['modlog_view'],
4812
						'href' => $scripturl . '?action=moderate;area=modlog',
4813
						'show' => !empty($modSettings['modlog_enabled']) && !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
4814
					),
4815
					'poststopics' => array(
4816
						'title' => $txt['mc_unapproved_poststopics'],
4817
						'href' => $scripturl . '?action=moderate;area=postmod;sa=posts',
4818
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
4819
					),
4820
					'attachments' => array(
4821
						'title' => $txt['mc_unapproved_attachments'],
4822
						'href' => $scripturl . '?action=moderate;area=attachmod;sa=attachments',
4823
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
4824
					),
4825
					'reports' => array(
4826
						'title' => $txt['mc_reported_posts'],
4827
						'href' => $scripturl . '?action=moderate;area=reportedposts',
4828
						'show' => !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
4829
					),
4830
					'reported_members' => array(
4831
						'title' => $txt['mc_reported_members'],
4832
						'href' => $scripturl . '?action=moderate;area=reportedmembers',
4833
						'show' => allowedTo('moderate_forum'),
4834
						'is_last' => true,
4835
					)
4836
				),
4837
			),
4838
			'calendar' => array(
4839
				'title' => $txt['calendar'],
4840
				'href' => $scripturl . '?action=calendar',
4841
				'show' => $context['allow_calendar'],
4842
				'sub_buttons' => array(
4843
					'view' => array(
4844
						'title' => $txt['calendar_menu'],
4845
						'href' => $scripturl . '?action=calendar',
4846
						'show' => $context['allow_calendar_event'],
4847
					),
4848
					'post' => array(
4849
						'title' => $txt['calendar_post_event'],
4850
						'href' => $scripturl . '?action=calendar;sa=post',
4851
						'show' => $context['allow_calendar_event'],
4852
						'is_last' => true,
4853
					),
4854
				),
4855
			),
4856
			'mlist' => array(
4857
				'title' => $txt['members_title'],
4858
				'href' => $scripturl . '?action=mlist',
4859
				'show' => $context['allow_memberlist'],
4860
				'sub_buttons' => array(
4861
					'mlist_view' => array(
4862
						'title' => $txt['mlist_menu_view'],
4863
						'href' => $scripturl . '?action=mlist',
4864
						'show' => true,
4865
					),
4866
					'mlist_search' => array(
4867
						'title' => $txt['mlist_search'],
4868
						'href' => $scripturl . '?action=mlist;sa=search',
4869
						'show' => true,
4870
						'is_last' => true,
4871
					),
4872
				),
4873
				'is_last' => !$context['right_to_left'] && (!$user_info['is_guest'] || !$context['can_register']),
4874
			),
4875
			'signup' => array(
4876
				'title' => $txt['register'],
4877
				'href' => $scripturl . '?action=signup',
4878
				'show' => $user_info['is_guest'] && $context['can_register'],
4879
				'sub_buttons' => array(
4880
				),
4881
				'is_last' => !$context['right_to_left'],
4882
			),
4883
		);
4884
4885
		// Allow editing menu buttons easily.
4886
		call_integration_hook('integrate_menu_buttons', array(&$buttons));
4887
4888
		// Now we put the buttons in the context so the theme can use them.
4889
		$menu_buttons = array();
4890
		foreach ($buttons as $act => $button)
4891
			if (!empty($button['show']))
4892
			{
4893
				$button['active_button'] = false;
4894
4895
				// This button needs some action.
4896
				if (isset($button['action_hook']))
4897
					$needs_action_hook = true;
4898
4899
				// Make sure the last button truly is the last button.
4900
				if (!empty($button['is_last']))
4901
				{
4902
					if (isset($last_button))
4903
						unset($menu_buttons[$last_button]['is_last']);
4904
					$last_button = $act;
4905
				}
4906
4907
				// Go through the sub buttons if there are any.
4908
				if (!empty($button['sub_buttons']))
4909
					foreach ($button['sub_buttons'] as $key => $subbutton)
4910
					{
4911
						if (empty($subbutton['show']))
4912
							unset($button['sub_buttons'][$key]);
4913
4914
						// 2nd level sub buttons next...
4915
						if (!empty($subbutton['sub_buttons']))
4916
						{
4917
							foreach ($subbutton['sub_buttons'] as $key2 => $sub_button2)
4918
							{
4919
								if (empty($sub_button2['show']))
4920
									unset($button['sub_buttons'][$key]['sub_buttons'][$key2]);
4921
							}
4922
						}
4923
					}
4924
4925
				// Does this button have its own icon?
4926
				if (isset($button['icon']) && file_exists($settings['theme_dir'] . '/images/' . $button['icon']))
4927
					$button['icon'] = '<img src="' . $settings['images_url'] . '/' . $button['icon'] . '" alt="">';
4928
				elseif (isset($button['icon']) && file_exists($settings['default_theme_dir'] . '/images/' . $button['icon']))
4929
					$button['icon'] = '<img src="' . $settings['default_images_url'] . '/' . $button['icon'] . '" alt="">';
4930
				elseif (isset($button['icon']))
4931
					$button['icon'] = '<span class="main_icons ' . $button['icon'] . '"></span>';
4932
				else
4933
					$button['icon'] = '<span class="main_icons ' . $act . '"></span>';
4934
4935
				$menu_buttons[$act] = $button;
4936
			}
4937
4938
		if (!empty($cache_enable) && $cache_enable >= 2)
4939
			cache_put_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $menu_buttons, $cacheTime);
4940
	}
4941
4942
	$context['menu_buttons'] = $menu_buttons;
4943
4944
	// Logging out requires the session id in the url.
4945
	if (isset($context['menu_buttons']['logout']))
4946
		$context['menu_buttons']['logout']['href'] = sprintf($context['menu_buttons']['logout']['href'], $context['session_var'], $context['session_id']);
4947
4948
	// Figure out which action we are doing so we can set the active tab.
4949
	// Default to home.
4950
	$current_action = 'home';
4951
4952
	if (isset($context['menu_buttons'][$context['current_action']]))
4953
		$current_action = $context['current_action'];
4954
	elseif ($context['current_action'] == 'search2')
4955
		$current_action = 'search';
4956
	elseif ($context['current_action'] == 'theme')
4957
		$current_action = isset($_REQUEST['sa']) && $_REQUEST['sa'] == 'pick' ? 'profile' : 'admin';
4958
	elseif ($context['current_action'] == 'register2')
4959
		$current_action = 'register';
4960
	elseif ($context['current_action'] == 'login2' || ($user_info['is_guest'] && $context['current_action'] == 'reminder'))
4961
		$current_action = 'login';
4962
	elseif ($context['current_action'] == 'groups' && $context['allow_moderation_center'])
4963
		$current_action = 'moderate';
4964
4965
	// There are certain exceptions to the above where we don't want anything on the menu highlighted.
4966
	if ($context['current_action'] == 'profile' && !empty($context['user']['is_owner']))
4967
	{
4968
		$current_action = !empty($_GET['area']) && $_GET['area'] == 'showalerts' ? 'self_alerts' : 'self_profile';
4969
		$context[$current_action] = true;
4970
	}
4971
	elseif ($context['current_action'] == 'pm')
4972
	{
4973
		$current_action = 'self_pm';
4974
		$context['self_pm'] = true;
4975
	}
4976
4977
	$context['total_mod_reports'] = 0;
4978
	$context['total_admin_reports'] = 0;
4979
4980
	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']))
4981
	{
4982
		$context['total_mod_reports'] = $context['open_mod_reports'];
4983
		$context['menu_buttons']['moderate']['sub_buttons']['reports']['amt'] = $context['open_mod_reports'];
4984
	}
4985
4986
	// Show how many errors there are
4987
	if (!empty($context['menu_buttons']['admin']['sub_buttons']['errorlog']))
4988
	{
4989
		// Get an error count, if necessary
4990
		if (!isset($context['num_errors']))
4991
		{
4992
			$query = $smcFunc['db_query']('', '
4993
				SELECT COUNT(*)
4994
				FROM {db_prefix}log_errors',
4995
				array()
4996
			);
4997
4998
			list($context['num_errors']) = $smcFunc['db_fetch_row']($query);
4999
			$smcFunc['db_free_result']($query);
5000
		}
5001
5002
		if (!empty($context['num_errors']))
5003
		{
5004
			$context['total_admin_reports'] += $context['num_errors'];
5005
			$context['menu_buttons']['admin']['sub_buttons']['errorlog']['amt'] = $context['num_errors'];
5006
		}
5007
	}
5008
5009
	// Show number of reported members
5010
	if (!empty($context['open_member_reports']) && !empty($context['menu_buttons']['moderate']['sub_buttons']['reported_members']))
5011
	{
5012
		$context['total_mod_reports'] += $context['open_member_reports'];
5013
		$context['menu_buttons']['moderate']['sub_buttons']['reported_members']['amt'] = $context['open_member_reports'];
5014
	}
5015
5016
	if (!empty($context['unapproved_members']) && !empty($context['menu_buttons']['admin']))
5017
	{
5018
		$context['menu_buttons']['admin']['sub_buttons']['memberapprove']['amt'] = $context['unapproved_members'];
5019
		$context['total_admin_reports'] += $context['unapproved_members'];
5020
	}
5021
5022
	if ($context['total_admin_reports'] > 0 && !empty($context['menu_buttons']['admin']))
5023
	{
5024
		$context['menu_buttons']['admin']['amt'] = $context['total_admin_reports'];
5025
	}
5026
5027
	// Do we have any open reports?
5028
	if ($context['total_mod_reports'] > 0 && !empty($context['menu_buttons']['moderate']))
5029
	{
5030
		$context['menu_buttons']['moderate']['amt'] = $context['total_mod_reports'];
5031
	}
5032
5033
	// Not all actions are simple.
5034
	if (!empty($needs_action_hook))
5035
		call_integration_hook('integrate_current_action', array(&$current_action));
5036
5037
	if (isset($context['menu_buttons'][$current_action]))
5038
		$context['menu_buttons'][$current_action]['active_button'] = true;
5039
}
5040
5041
/**
5042
 * Generate a random seed and ensure it's stored in settings.
5043
 */
5044
function smf_seed_generator()
5045
{
5046
	updateSettings(array('rand_seed' => microtime(true)));
5047
}
5048
5049
/**
5050
 * Process functions of an integration hook.
5051
 * calls all functions of the given hook.
5052
 * supports static class method calls.
5053
 *
5054
 * @param string $hook The hook name
5055
 * @param array $parameters An array of parameters this hook implements
5056
 * @return array The results of the functions
5057
 */
5058
function call_integration_hook($hook, $parameters = array())
5059
{
5060
	global $modSettings, $settings, $boarddir, $sourcedir, $db_show_debug;
5061
	global $context, $txt;
5062
5063
	if ($db_show_debug === true)
5064
		$context['debug']['hooks'][] = $hook;
5065
5066
	// Need to have some control.
5067
	if (!isset($context['instances']))
5068
		$context['instances'] = array();
5069
5070
	$results = array();
5071
	if (empty($modSettings[$hook]))
5072
		return $results;
5073
5074
	$functions = explode(',', $modSettings[$hook]);
5075
	// Loop through each function.
5076
	foreach ($functions as $function)
5077
	{
5078
		// Hook has been marked as "disabled". Skip it!
5079
		if (strpos($function, '!') !== false)
5080
			continue;
5081
5082
		$call = call_helper($function, true);
5083
5084
		// Is it valid?
5085
		if (!empty($call))
5086
			$results[$function] = call_user_func_array($call, $parameters);
0 ignored issues
show
Bug introduced by
It seems like $call can also be of type boolean; however, parameter $callback of call_user_func_array() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

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

5086
			$results[$function] = call_user_func_array(/** @scrutinizer ignore-type */ $call, $parameters);
Loading history...
5087
		// This failed, but we want to do so silently.
5088
		elseif (!empty($function) && !empty($context['ignore_hook_errors']))
5089
			return $results;
5090
		// Whatever it was suppose to call, it failed :(
5091
		elseif (!empty($function))
5092
		{
5093
			loadLanguage('Errors');
5094
5095
			// Get a full path to show on error.
5096
			if (strpos($function, '|') !== false)
5097
			{
5098
				list ($file, $string) = explode('|', $function);
5099
				$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'])));
5100
				log_error(sprintf($txt['hook_fail_call_to'], $string, $absPath), 'general');
5101
			}
5102
			// "Assume" the file resides on $boarddir somewhere...
5103
			else
5104
				log_error(sprintf($txt['hook_fail_call_to'], $function, $boarddir), 'general');
5105
		}
5106
	}
5107
5108
	return $results;
5109
}
5110
5111
/**
5112
 * Add a function for integration hook.
5113
 * does nothing if the function is already added.
5114
 *
5115
 * @param string $hook The complete hook name.
5116
 * @param string $function The function name. Can be a call to a method via Class::method.
5117
 * @param bool $permanent If true, updates the value in settings table.
5118
 * @param string $file The file. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
5119
 * @param bool $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
5120
 */
5121
function add_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
5122
{
5123
	global $smcFunc, $modSettings;
5124
5125
	// Any objects?
5126
	if ($object)
5127
		$function = $function . '#';
5128
5129
	// Any files  to load?
5130
	if (!empty($file) && is_string($file))
5131
		$function = $file . (!empty($function) ? '|' . $function : '');
5132
5133
	// Get the correct string.
5134
	$integration_call = $function;
5135
5136
	// Is it going to be permanent?
5137
	if ($permanent)
5138
	{
5139
		$request = $smcFunc['db_query']('', '
5140
			SELECT value
5141
			FROM {db_prefix}settings
5142
			WHERE variable = {string:variable}',
5143
			array(
5144
				'variable' => $hook,
5145
			)
5146
		);
5147
		list ($current_functions) = $smcFunc['db_fetch_row']($request);
5148
		$smcFunc['db_free_result']($request);
5149
5150
		if (!empty($current_functions))
5151
		{
5152
			$current_functions = explode(',', $current_functions);
5153
			if (in_array($integration_call, $current_functions))
5154
				return;
5155
5156
			$permanent_functions = array_merge($current_functions, array($integration_call));
5157
		}
5158
		else
5159
			$permanent_functions = array($integration_call);
5160
5161
		updateSettings(array($hook => implode(',', $permanent_functions)));
5162
	}
5163
5164
	// Make current function list usable.
5165
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
5166
5167
	// Do nothing, if it's already there.
5168
	if (in_array($integration_call, $functions))
5169
		return;
5170
5171
	$functions[] = $integration_call;
5172
	$modSettings[$hook] = implode(',', $functions);
5173
}
5174
5175
/**
5176
 * Remove an integration hook function.
5177
 * Removes the given function from the given hook.
5178
 * Does nothing if the function is not available.
5179
 *
5180
 * @param string $hook The complete hook name.
5181
 * @param string $function The function name. Can be a call to a method via Class::method.
5182
 * @param boolean $permanent Irrelevant for the function itself but need to declare it to match
5183
 * @param string $file The filename. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
5184
 * @param boolean $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
5185
 * @see add_integration_function
5186
 */
5187
function remove_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
5188
{
5189
	global $smcFunc, $modSettings;
5190
5191
	// Any objects?
5192
	if ($object)
5193
		$function = $function . '#';
5194
5195
	// Any files  to load?
5196
	if (!empty($file) && is_string($file))
5197
		$function = $file . '|' . $function;
5198
5199
	// Get the correct string.
5200
	$integration_call = $function;
5201
5202
	// Get the permanent functions.
5203
	$request = $smcFunc['db_query']('', '
5204
		SELECT value
5205
		FROM {db_prefix}settings
5206
		WHERE variable = {string:variable}',
5207
		array(
5208
			'variable' => $hook,
5209
		)
5210
	);
5211
	list ($current_functions) = $smcFunc['db_fetch_row']($request);
5212
	$smcFunc['db_free_result']($request);
5213
5214
	if (!empty($current_functions))
5215
	{
5216
		$current_functions = explode(',', $current_functions);
5217
5218
		if (in_array($integration_call, $current_functions))
5219
			updateSettings(array($hook => implode(',', array_diff($current_functions, array($integration_call)))));
5220
	}
5221
5222
	// Turn the function list into something usable.
5223
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
5224
5225
	// You can only remove it if it's available.
5226
	if (!in_array($integration_call, $functions))
5227
		return;
5228
5229
	$functions = array_diff($functions, array($integration_call));
5230
	$modSettings[$hook] = implode(',', $functions);
5231
}
5232
5233
/**
5234
 * Receives a string and tries to figure it out if its a method or a function.
5235
 * If a method is found, it looks for a "#" which indicates SMF should create a new instance of the given class.
5236
 * Checks the string/array for is_callable() and return false/fatal_lang_error is the given value results in a non callable string/array.
5237
 * Prepare and returns a callable depending on the type of method/function found.
5238
 *
5239
 * @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)
5240
 * @param boolean $return If true, the function will not call the function/method but instead will return the formatted string.
5241
 * @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.
5242
 */
5243
function call_helper($string, $return = false)
5244
{
5245
	global $context, $smcFunc, $txt, $db_show_debug;
5246
5247
	// Really?
5248
	if (empty($string))
5249
		return false;
5250
5251
	// An array? should be a "callable" array IE array(object/class, valid_callable).
5252
	// A closure? should be a callable one.
5253
	if (is_array($string) || $string instanceof Closure)
5254
		return $return ? $string : (is_callable($string) ? call_user_func($string) : false);
5255
5256
	// No full objects, sorry! pass a method or a property instead!
5257
	if (is_object($string))
5258
		return false;
5259
5260
	// Stay vitaminized my friends...
5261
	$string = $smcFunc['htmlspecialchars']($smcFunc['htmltrim']($string));
5262
5263
	// Is there a file to load?
5264
	$string = load_file($string);
5265
5266
	// Loaded file failed
5267
	if (empty($string))
5268
		return false;
5269
5270
	// Found a method.
5271
	if (strpos($string, '::') !== false)
0 ignored issues
show
Bug introduced by
It seems like $string can also be of type boolean; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

5271
	if (strpos(/** @scrutinizer ignore-type */ $string, '::') !== false)
Loading history...
5272
	{
5273
		list ($class, $method) = explode('::', $string);
0 ignored issues
show
Bug introduced by
It seems like $string can also be of type boolean; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

5273
		list ($class, $method) = explode('::', /** @scrutinizer ignore-type */ $string);
Loading history...
5274
5275
		// Check if a new object will be created.
5276
		if (strpos($method, '#') !== false)
5277
		{
5278
			// Need to remove the # thing.
5279
			$method = str_replace('#', '', $method);
5280
5281
			// Don't need to create a new instance for every method.
5282
			if (empty($context['instances'][$class]) || !($context['instances'][$class] instanceof $class))
5283
			{
5284
				$context['instances'][$class] = new $class;
5285
5286
				// Add another one to the list.
5287
				if ($db_show_debug === true)
5288
				{
5289
					if (!isset($context['debug']['instances']))
5290
						$context['debug']['instances'] = array();
5291
5292
					$context['debug']['instances'][$class] = $class;
5293
				}
5294
			}
5295
5296
			$func = array($context['instances'][$class], $method);
5297
		}
5298
5299
		// Right then. This is a call to a static method.
5300
		else
5301
			$func = array($class, $method);
5302
	}
5303
5304
	// Nope! just a plain regular function.
5305
	else
5306
		$func = $string;
5307
5308
	// We can't call this helper, but we want to silently ignore this.
5309
	if (!is_callable($func, false, $callable_name) && !empty($context['ignore_hook_errors']))
5310
		return false;
5311
5312
	// Right, we got what we need, time to do some checks.
5313
	elseif (!is_callable($func, false, $callable_name))
5314
	{
5315
		loadLanguage('Errors');
5316
		log_error(sprintf($txt['sub_action_fail'], $callable_name), 'general');
5317
5318
		// Gotta tell everybody.
5319
		return false;
5320
	}
5321
5322
	// Everything went better than expected.
5323
	else
5324
	{
5325
		// What are we gonna do about it?
5326
		if ($return)
5327
			return $func;
5328
5329
		// If this is a plain function, avoid the heat of calling call_user_func().
5330
		else
5331
		{
5332
			if (is_array($func))
5333
				call_user_func($func);
5334
5335
			else
5336
				$func();
5337
		}
5338
	}
5339
}
5340
5341
/**
5342
 * Receives a string and tries to figure it out if it contains info to load a file.
5343
 * Checks for a | (pipe) symbol and tries to load a file with the info given.
5344
 * 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.
5345
 *
5346
 * @param string $string The string containing a valid format.
5347
 * @return string|boolean The given string with the pipe and file info removed. Boolean false if the file couldn't be loaded.
5348
 */
5349
function load_file($string)
5350
{
5351
	global $sourcedir, $txt, $boarddir, $settings, $context;
5352
5353
	if (empty($string))
5354
		return false;
5355
5356
	if (strpos($string, '|') !== false)
5357
	{
5358
		list ($file, $string) = explode('|', $string);
5359
5360
		// Match the wildcards to their regular vars.
5361
		if (empty($settings['theme_dir']))
5362
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
5363
5364
		else
5365
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
5366
5367
		// Load the file if it can be loaded.
5368
		if (file_exists($absPath))
5369
			require_once($absPath);
5370
5371
		// No? try a fallback to $sourcedir
5372
		else
5373
		{
5374
			$absPath = $sourcedir . '/' . $file;
5375
5376
			if (file_exists($absPath))
5377
				require_once($absPath);
5378
5379
			// Sorry, can't do much for you at this point.
5380
			elseif (empty($context['uninstalling']))
5381
			{
5382
				loadLanguage('Errors');
5383
				log_error(sprintf($txt['hook_fail_loading_file'], $absPath), 'general');
5384
5385
				// File couldn't be loaded.
5386
				return false;
5387
			}
5388
		}
5389
	}
5390
5391
	return $string;
5392
}
5393
5394
/**
5395
 * Get the contents of a URL, irrespective of allow_url_fopen.
5396
 *
5397
 * - reads the contents of an http or ftp address and returns the page in a string
5398
 * - will accept up to 3 page redirections (redirectio_level in the function call is private)
5399
 * - if post_data is supplied, the value and length is posted to the given url as form data
5400
 * - URL must be supplied in lowercase
5401
 *
5402
 * @param string $url The URL
5403
 * @param string $post_data The data to post to the given URL
5404
 * @param bool $keep_alive Whether to send keepalive info
5405
 * @param int $redirection_level How many levels of redirection
5406
 * @return string|false The fetched data or false on failure
5407
 */
5408
function fetch_web_data($url, $post_data = '', $keep_alive = false, $redirection_level = 0)
5409
{
5410
	global $webmaster_email, $sourcedir, $txt;
5411
	static $keep_alive_dom = null, $keep_alive_fp = null;
5412
5413
	preg_match('~^(http|ftp)(s)?://([^/:]+)(:(\d+))?(.+)$~', $url, $match);
5414
5415
	// No scheme? No data for you!
5416
	if (empty($match[1]))
5417
		return false;
5418
5419
	// An FTP url. We should try connecting and RETRieving it...
5420
	elseif ($match[1] == 'ftp')
5421
	{
5422
		// Include the file containing the ftp_connection class.
5423
		require_once($sourcedir . '/Class-Package.php');
5424
5425
		// Establish a connection and attempt to enable passive mode.
5426
		$ftp = new ftp_connection(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? 21 : $match[5], 'anonymous', $webmaster_email);
5427
		if ($ftp->error !== false || !$ftp->passive())
0 ignored issues
show
introduced by
The condition $ftp->error !== false is always true.
Loading history...
5428
			return false;
5429
5430
		// I want that one *points*!
5431
		fwrite($ftp->connection, 'RETR ' . $match[6] . "\r\n");
5432
5433
		// Since passive mode worked (or we would have returned already!) open the connection.
5434
		$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...
5435
		if (!$fp)
5436
			return false;
5437
5438
		// The server should now say something in acknowledgement.
5439
		$ftp->check_response(150);
5440
5441
		$data = '';
5442
		while (!feof($fp))
5443
			$data .= fread($fp, 4096);
5444
		fclose($fp);
5445
5446
		// All done, right?  Good.
5447
		$ftp->check_response(226);
5448
		$ftp->close();
5449
	}
5450
5451
	// This is more likely; a standard HTTP URL.
5452
	elseif (isset($match[1]) && $match[1] == 'http')
5453
	{
5454
		// First try to use fsockopen, because it is fastest.
5455
		if ($keep_alive && $match[3] == $keep_alive_dom)
5456
			$fp = $keep_alive_fp;
5457
		if (empty($fp))
5458
		{
5459
			// Open the socket on the port we want...
5460
			$fp = @fsockopen(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? ($match[2] ? 443 : 80) : $match[5], $err, $err, 5);
5461
		}
5462
		if (!empty($fp))
5463
		{
5464
			if ($keep_alive)
5465
			{
5466
				$keep_alive_dom = $match[3];
5467
				$keep_alive_fp = $fp;
5468
			}
5469
5470
			// I want this, from there, and I'm not going to be bothering you for more (probably.)
5471
			if (empty($post_data))
5472
			{
5473
				fwrite($fp, 'GET ' . ($match[6] !== '/' ? str_replace(' ', '%20', $match[6]) : '') . ' HTTP/1.0' . "\r\n");
5474
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
5475
				fwrite($fp, 'user-agent: '. SMF_USER_AGENT . "\r\n");
5476
				if ($keep_alive)
5477
					fwrite($fp, 'connection: Keep-Alive' . "\r\n\r\n");
5478
				else
5479
					fwrite($fp, 'connection: close' . "\r\n\r\n");
5480
			}
5481
			else
5482
			{
5483
				fwrite($fp, 'POST ' . ($match[6] !== '/' ? $match[6] : '') . ' HTTP/1.0' . "\r\n");
5484
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
5485
				fwrite($fp, 'user-agent: '. SMF_USER_AGENT . "\r\n");
5486
				if ($keep_alive)
5487
					fwrite($fp, 'connection: Keep-Alive' . "\r\n");
5488
				else
5489
					fwrite($fp, 'connection: close' . "\r\n");
5490
				fwrite($fp, 'content-type: application/x-www-form-urlencoded' . "\r\n");
5491
				fwrite($fp, 'content-length: ' . strlen($post_data) . "\r\n\r\n");
5492
				fwrite($fp, $post_data);
5493
			}
5494
5495
			$response = fgets($fp, 768);
5496
5497
			// Redirect in case this location is permanently or temporarily moved.
5498
			if ($redirection_level < 3 && preg_match('~^HTTP/\S+\s+30[127]~i', $response) === 1)
5499
			{
5500
				$header = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $header is dead and can be removed.
Loading history...
5501
				$location = '';
5502
				while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
5503
					if (stripos($header, 'location:') !== false)
5504
						$location = trim(substr($header, strpos($header, ':') + 1));
5505
5506
				if (empty($location))
5507
					return false;
5508
				else
5509
				{
5510
					if (!$keep_alive)
5511
						fclose($fp);
5512
					return fetch_web_data($location, $post_data, $keep_alive, $redirection_level + 1);
5513
				}
5514
			}
5515
5516
			// Make sure we get a 200 OK.
5517
			elseif (preg_match('~^HTTP/\S+\s+20[01]~i', $response) === 0)
5518
				return false;
5519
5520
			// Skip the headers...
5521
			while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
5522
			{
5523
				if (preg_match('~content-length:\s*(\d+)~i', $header, $match) != 0)
5524
					$content_length = $match[1];
5525
				elseif (preg_match('~connection:\s*close~i', $header) != 0)
5526
				{
5527
					$keep_alive_dom = null;
5528
					$keep_alive = false;
5529
				}
5530
5531
				continue;
5532
			}
5533
5534
			$data = '';
5535
			if (isset($content_length))
5536
			{
5537
				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...
5538
					$data .= fread($fp, $content_length - strlen($data));
5539
			}
5540
			else
5541
			{
5542
				while (!feof($fp))
5543
					$data .= fread($fp, 4096);
5544
			}
5545
5546
			if (!$keep_alive)
5547
				fclose($fp);
5548
		}
5549
5550
		// If using fsockopen didn't work, try to use cURL if available.
5551
		elseif (function_exists('curl_init'))
5552
		{
5553
			// Include the file containing the curl_fetch_web_data class.
5554
			require_once($sourcedir . '/Class-CurlFetchWeb.php');
5555
5556
			$fetch_data = new curl_fetch_web_data();
5557
			$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

5557
			$fetch_data->get_url_data($url, /** @scrutinizer ignore-type */ $post_data);
Loading history...
5558
5559
			// no errors and a 200 result, then we have a good dataset, well we at least have data. ;)
5560
			if ($fetch_data->result('code') == 200 && !$fetch_data->result('error'))
5561
				$data = $fetch_data->result('body');
5562
			else
5563
				return false;
5564
		}
5565
5566
		// Neither fsockopen nor curl are available. Well, phooey.
5567
		else
5568
			return false;
5569
	}
5570
	else
5571
	{
5572
		// Umm, this shouldn't happen?
5573
		loadLanguage('Errors');
5574
		trigger_error($txt['fetch_web_data_bad_url'], E_USER_NOTICE);
5575
		$data = false;
5576
	}
5577
5578
	return $data;
5579
}
5580
5581
/**
5582
 * Attempts to determine the MIME type of some data or a file.
5583
 *
5584
 * @param string $data The data to check, or the path or URL of a file to check.
5585
 * @param string $is_path If true, $data is a path or URL to a file.
5586
 * @return string|bool A MIME type, or false if we cannot determine it.
5587
 */
5588
function get_mime_type($data, $is_path = false)
5589
{
5590
	global $cachedir;
5591
5592
	$finfo_loaded = extension_loaded('fileinfo');
5593
	$exif_loaded = extension_loaded('exif') && function_exists('image_type_to_mime_type');
5594
5595
	// Oh well. We tried.
5596
	if (!$finfo_loaded && !$exif_loaded)
5597
		return false;
5598
5599
	// Start with the 'empty' MIME type.
5600
	$mime_type = 'application/x-empty';
5601
5602
	if ($finfo_loaded)
5603
	{
5604
		// Just some nice, simple data to analyze.
5605
		if (empty($is_path))
5606
			$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
5607
5608
		// A file, or maybe a URL?
5609
		else
5610
		{
5611
			// Local file.
5612
			if (file_exists($data))
5613
				$mime_type = mime_content_type($data);
5614
5615
			// URL.
5616
			elseif ($data = fetch_web_data($data))
5617
				$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
5618
		}
5619
	}
5620
	// Workaround using Exif requires a local file.
5621
	else
5622
	{
5623
		// If $data is a URL to fetch, do so.
5624
		if (!empty($is_path) && !file_exists($data) && url_exists($data))
5625
		{
5626
			$data = fetch_web_data($data);
5627
			$is_path = false;
5628
		}
5629
5630
		// If we don't have a local file, create one and use it.
5631
		if (empty($is_path))
5632
		{
5633
			$temp_file = tempnam($cachedir, md5($data));
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type false; however, parameter $string of md5() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

5633
			$temp_file = tempnam($cachedir, md5(/** @scrutinizer ignore-type */ $data));
Loading history...
5634
			file_put_contents($temp_file, $data);
5635
			$is_path = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $is_path is dead and can be removed.
Loading history...
5636
			$data = $temp_file;
5637
		}
5638
5639
		$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

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

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

5790
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5791
5792
	// remove left to right / right to left overrides
5793
	if ($num === 0x202D || $num === 0x202E)
5794
		return '';
5795
5796
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
5797
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
5798
		return '&#' . $num . ';';
5799
5800
	if (empty($context['utf8']))
5801
	{
5802
		// no control characters
5803
		if ($num < 0x20)
5804
			return '';
5805
		// text is text
5806
		elseif ($num < 0x80)
5807
			return chr($num);
0 ignored issues
show
Bug introduced by
It seems like $num can also be of type double; however, parameter $codepoint of chr() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

5807
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5808
		// all others get html-ised
5809
		else
5810
			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

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

5848
	$num = $matches[1][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[1], 1)) : (int) $matches[1];
Loading history...
5849
5850
	// <0x20 are control characters, > 0x10FFFF is past the end of the utf8 character set
5851
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text), 0x202D-E are left to right overrides
5852
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202D || $num === 0x202E)
5853
		return '';
5854
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
5855
	elseif ($num < 0x80)
5856
		return chr($num);
0 ignored issues
show
Bug introduced by
It seems like $num can also be of type double; however, parameter $codepoint of chr() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

}

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

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

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

Loading history...
5985
5986
	$normal_priority_tzids = array_diff(array_unique(array_merge(array_keys($tzid_metazones), timezone_identifiers_list())), $priority_tzids, $low_priority_tzids);
0 ignored issues
show
Bug introduced by
Are you sure the usage of timezone_identifiers_list() is correct as it seems to always return null.

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

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

}

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

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

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

Loading history...
Bug introduced by
timezone_identifiers_list() of type void is incompatible with the type array expected by parameter $arrays of array_merge(). ( Ignorable by Annotation )

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

5986
	$normal_priority_tzids = array_diff(array_unique(array_merge(array_keys($tzid_metazones), /** @scrutinizer ignore-type */ timezone_identifiers_list())), $priority_tzids, $low_priority_tzids);
Loading history...
5987
5988
	// Process them in order of importance.
5989
	$tzids = array_merge($priority_tzids, $normal_priority_tzids, $low_priority_tzids);
5990
5991
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
5992
	$dst_types = array();
5993
	$labels = array();
5994
	$offsets = array();
5995
	foreach ($tzids as $tzid)
5996
	{
5997
		// We don't want UTC right now
5998
		if ($tzid == 'UTC')
5999
			continue;
6000
6001
		$tz = @timezone_open($tzid);
6002
6003
		if ($tz == null)
6004
			continue;
6005
6006
		// First, get the set of transition rules for this tzid
6007
		$tzinfo = timezone_transitions_get($tz, $when, $later);
6008
6009
		// Use the entire set of transition rules as the array *key* so we can avoid duplicates
6010
		$tzkey = serialize($tzinfo);
6011
6012
		// ...But make sure to include all explicitly defined meta-zones.
6013
		if (isset($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid]))
6014
			$tzkey = serialize(array_merge($tzinfo, array('metazone' => $tzid_metazones[$tzid])));
6015
6016
		// Don't overwrite our preferred tzids
6017
		if (empty($zones[$tzkey]['tzid']))
6018
		{
6019
			$zones[$tzkey]['tzid'] = $tzid;
6020
			$zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
6021
6022
			foreach ($tzinfo as $transition) {
6023
				$zones[$tzkey]['abbrs'][] = $transition['abbr'];
6024
			}
6025
6026
			if (isset($tzid_metazones[$tzid]))
6027
				$zones[$tzkey]['metazone'] = $tzid_metazones[$tzid];
6028
			else
6029
			{
6030
				$tzgeo = timezone_location_get($tz);
6031
				$country_tzids = get_sorted_tzids_for_country($tzgeo['country_code']);
6032
6033
				if (count($country_tzids) === 1)
6034
					$zones[$tzkey]['metazone'] = $txt['iso3166'][$tzgeo['country_code']];
6035
			}
6036
		}
6037
6038
		// A time zone from a prioritized country?
6039
		if (in_array($tzid, $priority_tzids))
6040
			$priority_zones[$tzkey] = true;
6041
6042
		// Keep track of the location and offset for this tzid
6043
		if (!empty($txt[$tzid]))
6044
			$zones[$tzkey]['locations'][] = $txt[$tzid];
6045
		else
6046
		{
6047
			$tzid_parts = explode('/', $tzid);
6048
			$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
6049
		}
6050
		$offsets[$tzkey] = $tzinfo[0]['offset'];
6051
6052
		// Figure out the "meta-zone" info for the label
6053
		if (empty($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid]))
6054
		{
6055
			$zones[$tzkey]['metazone'] = $tzid_metazones[$tzid];
6056
			$zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
6057
		}
6058
		$dst_types[$tzkey] = count($tzinfo) > 1 ? 'c' : ($tzinfo[0]['isdst'] ? 't' : 'f');
6059
		$labels[$tzkey] = !empty($zones[$tzkey]['metazone']) && !empty($tztxt[$zones[$tzkey]['metazone']]) ? $tztxt[$zones[$tzkey]['metazone']] : '';
6060
6061
		// Remember this for later
6062
		if (isset($cur_profile['timezone']) && $cur_profile['timezone'] == $tzid)
6063
			$member_tzkey = $tzkey;
6064
		if (isset($context['event']['tz']) && $context['event']['tz'] == $tzid)
6065
			$event_tzkey = $tzkey;
6066
	}
6067
6068
	// Sort by offset, then label, then DST type.
6069
	array_multisort($offsets, SORT_ASC, SORT_NUMERIC, $labels, SORT_ASC, $dst_types, SORT_ASC, $zones);
0 ignored issues
show
Bug introduced by
SORT_NUMERIC cannot be passed to array_multisort() as the parameter $rest expects a reference. ( Ignorable by Annotation )

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

6069
	array_multisort($offsets, SORT_ASC, /** @scrutinizer ignore-type */ SORT_NUMERIC, $labels, SORT_ASC, $dst_types, SORT_ASC, $zones);
Loading history...
Comprehensibility Best Practice introduced by
The variable $zones does not seem to be defined for all execution paths leading up to this point.
Loading history...
Bug introduced by
SORT_ASC cannot be passed to array_multisort() as the parameter $rest expects a reference. ( Ignorable by Annotation )

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

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

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

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

}

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

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

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

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

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

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

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

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

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

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

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

6678
	elseif (!empty($modSettings['tld_regex']) && @preg_match('~' . $modSettings['tld_regex'] . '~', /** @scrutinizer ignore-type */ null) !== false)
Loading history...
6679
	{
6680
		$done = true;
6681
		return;
6682
	}
6683
6684
	// If we successfully got an update, process the list into an array
6685
	if (!empty($tlds))
6686
	{
6687
		// Clean $tlds and convert it to an array
6688
		$tlds = array_filter(explode("\n", strtolower($tlds)), function($line)
6689
		{
6690
			$line = trim($line);
6691
			if (empty($line) || strlen($line) != strspn($line, 'abcdefghijklmnopqrstuvwxyz0123456789-'))
6692
				return false;
6693
			else
6694
				return true;
6695
		});
6696
6697
		// Convert Punycode to Unicode
6698
		require_once($sourcedir . '/Class-Punycode.php');
6699
		$Punycode = new Punycode();
6700
		$tlds = array_map(function($input) use ($Punycode)
6701
		{
6702
			return $Punycode->decode($input);
6703
		}, $tlds);
6704
	}
6705
	// Otherwise, use the 2012 list of gTLDs and ccTLDs for now and schedule a background update
6706
	else
6707
	{
6708
		$tlds = array('com', 'net', 'org', 'edu', 'gov', 'mil', 'aero', 'asia', 'biz',
6709
			'cat', 'coop', 'info', 'int', 'jobs', 'mobi', 'museum', 'name', 'post',
6710
			'pro', 'tel', 'travel', 'xxx', 'ac', 'ad', 'ae', 'af', 'ag', 'ai', 'al',
6711
			'am', 'ao', 'aq', 'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd',
6712
			'be', 'bf', 'bg', 'bh', 'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv',
6713
			'bw', 'by', 'bz', 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm',
6714
			'cn', 'co', 'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do',
6715
			'dz', 'ec', 'ee', 'eg', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo',
6716
			'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp',
6717
			'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu',
6718
			'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm', 'jo',
6719
			'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la',
6720
			'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md',
6721
			'me', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp', 'mq', 'mr', 'ms', 'mt',
6722
			'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf', 'ng', 'ni', 'nl',
6723
			'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl',
6724
			'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw',
6725
			'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn',
6726
			'so', 'sr', 'ss', 'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg',
6727
			'th', 'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua',
6728
			'ug', 'uk', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf',
6729
			'ws', 'ye', 'yt', 'za', 'zm', 'zw',
6730
		);
6731
6732
		// Schedule a background update, unless civilization has collapsed and/or we are having connectivity issues.
6733
		if (empty($postapocalypticNightmare))
6734
		{
6735
			$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
6736
				array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
6737
				array('$sourcedir/tasks/UpdateTldRegex.php', 'Update_TLD_Regex', '', 0), array()
6738
			);
6739
		}
6740
	}
6741
6742
	// Tack on some "special use domain names" that aren't in DNS but may possibly resolve.
6743
	// See https://www.iana.org/assignments/special-use-domain-names/ for more info.
6744
	$tlds = array_merge($tlds, array('local', 'onion', 'test'));
6745
6746
	// Get an optimized regex to match all the TLDs
6747
	$tld_regex = build_regex($tlds);
6748
6749
	// Remember the new regex in $modSettings
6750
	updateSettings(array('tld_regex' => $tld_regex));
6751
6752
	// Redundant repetition is redundant
6753
	$done = true;
6754
}
6755
6756
/**
6757
 * Creates optimized regular expressions from an array of strings.
6758
 *
6759
 * An optimized regex built using this function will be much faster than a
6760
 * simple regex built using `implode('|', $strings)` --- anywhere from several
6761
 * times to several orders of magnitude faster.
6762
 *
6763
 * However, the time required to build the optimized regex is approximately
6764
 * equal to the time it takes to execute the simple regex. Therefore, it is only
6765
 * worth calling this function if the resulting regex will be used more than
6766
 * once.
6767
 *
6768
 * Because PHP places an upper limit on the allowed length of a regex, very
6769
 * large arrays of $strings may not fit in a single regex. Normally, the excess
6770
 * strings will simply be dropped. However, if the $returnArray parameter is set
6771
 * to true, this function will build as many regexes as necessary to accommodate
6772
 * everything in $strings and return them in an array. You will need to iterate
6773
 * through all elements of the returned array in order to test all possible
6774
 * matches.
6775
 *
6776
 * @param array $strings An array of strings to make a regex for.
6777
 * @param string $delim An optional delimiter character to pass to preg_quote().
6778
 * @param bool $returnArray If true, returns an array of regexes.
6779
 * @return string|array One or more regular expressions to match any of the input strings.
6780
 */
6781
function build_regex($strings, $delim = null, $returnArray = false)
6782
{
6783
	global $smcFunc;
6784
	static $regexes = array();
6785
6786
	// If it's not an array, there's not much to do. ;)
6787
	if (!is_array($strings))
0 ignored issues
show
introduced by
The condition is_array($strings) is always true.
Loading history...
6788
		return preg_quote(@strval($strings), $delim);
6789
6790
	$regex_key = md5(json_encode(array($strings, $delim, $returnArray)));
6791
6792
	if (isset($regexes[$regex_key]))
6793
		return $regexes[$regex_key];
6794
6795
	// The mb_* functions are faster than the $smcFunc ones, but may not be available
6796
	if (function_exists('mb_internal_encoding') && function_exists('mb_detect_encoding') && function_exists('mb_strlen') && function_exists('mb_substr'))
6797
	{
6798
		if (($string_encoding = mb_detect_encoding(implode(' ', $strings))) !== false)
6799
		{
6800
			$current_encoding = mb_internal_encoding();
6801
			mb_internal_encoding($string_encoding);
6802
		}
6803
6804
		$strlen = 'mb_strlen';
6805
		$substr = 'mb_substr';
6806
	}
6807
	else
6808
	{
6809
		$strlen = $smcFunc['strlen'];
6810
		$substr = $smcFunc['substr'];
6811
	}
6812
6813
	// This recursive function creates the index array from the strings
6814
	$add_string_to_index = function($string, $index) use (&$strlen, &$substr, &$add_string_to_index)
6815
	{
6816
		static $depth = 0;
6817
		$depth++;
6818
6819
		$first = (string) @$substr($string, 0, 1);
6820
6821
		// No first character? That's no good.
6822
		if ($first === '')
6823
		{
6824
			// A nested array? Really? Ugh. Fine.
6825
			if (is_array($string) && $depth < 20)
6826
			{
6827
				foreach ($string as $str)
6828
					$index = $add_string_to_index($str, $index);
6829
			}
6830
6831
			$depth--;
6832
			return $index;
6833
		}
6834
6835
		if (empty($index[$first]))
6836
			$index[$first] = array();
6837
6838
		if ($strlen($string) > 1)
6839
		{
6840
			// Sanity check on recursion
6841
			if ($depth > 99)
6842
				$index[$first][$substr($string, 1)] = '';
6843
6844
			else
6845
				$index[$first] = $add_string_to_index($substr($string, 1), $index[$first]);
6846
		}
6847
		else
6848
			$index[$first][''] = '';
6849
6850
		$depth--;
6851
		return $index;
6852
	};
6853
6854
	// This recursive function turns the index array into a regular expression
6855
	$index_to_regex = function(&$index, $delim) use (&$strlen, &$index_to_regex)
6856
	{
6857
		static $depth = 0;
6858
		$depth++;
6859
6860
		// Absolute max length for a regex is 32768, but we might need wiggle room
6861
		$max_length = 30000;
6862
6863
		$regex = array();
6864
		$length = 0;
6865
6866
		foreach ($index as $key => $value)
6867
		{
6868
			$key_regex = preg_quote($key, $delim);
6869
			$new_key = $key;
6870
6871
			if (empty($value))
6872
				$sub_regex = '';
6873
			else
6874
			{
6875
				$sub_regex = $index_to_regex($value, $delim);
6876
6877
				if (count(array_keys($value)) == 1)
6878
				{
6879
					$new_key_array = explode('(?' . '>', $sub_regex);
6880
					$new_key .= $new_key_array[0];
6881
				}
6882
				else
6883
					$sub_regex = '(?' . '>' . $sub_regex . ')';
6884
			}
6885
6886
			if ($depth > 1)
6887
				$regex[$new_key] = $key_regex . $sub_regex;
6888
			else
6889
			{
6890
				if (($length += strlen($key_regex) + 1) < $max_length || empty($regex))
6891
				{
6892
					$regex[$new_key] = $key_regex . $sub_regex;
6893
					unset($index[$key]);
6894
				}
6895
				else
6896
					break;
6897
			}
6898
		}
6899
6900
		// Sort by key length and then alphabetically
6901
		uksort($regex, function($k1, $k2) use (&$strlen)
6902
		{
6903
			$l1 = $strlen($k1);
6904
			$l2 = $strlen($k2);
6905
6906
			if ($l1 == $l2)
6907
				return strcmp($k1, $k2) > 0 ? 1 : -1;
6908
			else
6909
				return $l1 > $l2 ? -1 : 1;
6910
		});
6911
6912
		$depth--;
6913
		return implode('|', $regex);
6914
	};
6915
6916
	// Now that the functions are defined, let's do this thing
6917
	$index = array();
6918
	$regex = '';
6919
6920
	foreach ($strings as $string)
6921
		$index = $add_string_to_index($string, $index);
6922
6923
	if ($returnArray === true)
6924
	{
6925
		$regex = array();
6926
		while (!empty($index))
6927
			$regex[] = '(?' . '>' . $index_to_regex($index, $delim) . ')';
6928
	}
6929
	else
6930
		$regex = '(?' . '>' . $index_to_regex($index, $delim) . ')';
6931
6932
	// Restore PHP's internal character encoding to whatever it was originally
6933
	if (!empty($current_encoding))
6934
		mb_internal_encoding($current_encoding);
6935
6936
	$regexes[$regex_key] = $regex;
6937
	return $regex;
6938
}
6939
6940
/**
6941
 * Check if the passed url has an SSL certificate.
6942
 *
6943
 * Returns true if a cert was found & false if not.
6944
 *
6945
 * @param string $url to check, in $boardurl format (no trailing slash).
6946
 */
6947
function ssl_cert_found($url)
6948
{
6949
	// This check won't work without OpenSSL
6950
	if (!extension_loaded('openssl'))
6951
		return true;
6952
6953
	// First, strip the subfolder from the passed url, if any
6954
	$parsedurl = parse_url($url);
6955
	$url = 'ssl://' . $parsedurl['host'] . ':443';
6956
6957
	// Next, check the ssl stream context for certificate info
6958
	if (version_compare(PHP_VERSION, '5.6.0', '<'))
6959
		$ssloptions = array("capture_peer_cert" => true);
6960
	else
6961
		$ssloptions = array("capture_peer_cert" => true, "verify_peer" => true, "allow_self_signed" => true);
6962
6963
	$result = false;
6964
	$context = stream_context_create(array("ssl" => $ssloptions));
6965
	$stream = @stream_socket_client($url, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
6966
	if ($stream !== false)
6967
	{
6968
		$params = stream_context_get_params($stream);
6969
		$result = isset($params["options"]["ssl"]["peer_certificate"]) ? true : false;
6970
	}
6971
	return $result;
6972
}
6973
6974
/**
6975
 * Check if the passed url has a redirect to https:// by querying headers.
6976
 *
6977
 * Returns true if a redirect was found & false if not.
6978
 * Note that when force_ssl = 2, SMF issues its own redirect...  So if this
6979
 * returns true, it may be caused by SMF, not necessarily an .htaccess redirect.
6980
 *
6981
 * @param string $url to check, in $boardurl format (no trailing slash).
6982
 */
6983
function https_redirect_active($url)
6984
{
6985
	// Ask for the headers for the passed url, but via http...
6986
	// Need to add the trailing slash, or it puts it there & thinks there's a redirect when there isn't...
6987
	$url = str_ireplace('https://', 'http://', $url) . '/';
0 ignored issues
show
Bug introduced by
Are you sure str_ireplace('https://', 'http://', $url) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

6987
	$url = /** @scrutinizer ignore-type */ str_ireplace('https://', 'http://', $url) . '/';
Loading history...
6988
	$headers = @get_headers($url);
6989
	if ($headers === false)
6990
		return false;
6991
6992
	// Now to see if it came back https...
6993
	// First check for a redirect status code in first row (301, 302, 307)
6994
	if (strstr($headers[0], '301') === false && strstr($headers[0], '302') === false && strstr($headers[0], '307') === false)
6995
		return false;
6996
6997
	// Search for the location entry to confirm https
6998
	$result = false;
6999
	foreach ($headers as $header)
7000
	{
7001
		if (stristr($header, 'Location: https://') !== false)
7002
		{
7003
			$result = true;
7004
			break;
7005
		}
7006
	}
7007
	return $result;
7008
}
7009
7010
/**
7011
 * Build query_wanna_see_board and query_see_board for a userid
7012
 *
7013
 * Returns array with keys query_wanna_see_board and query_see_board
7014
 *
7015
 * @param int $userid of the user
7016
 */
7017
function build_query_board($userid)
7018
{
7019
	global $user_info, $modSettings, $smcFunc, $db_prefix;
7020
7021
	$query_part = array();
7022
7023
	// If we come from cron, we can't have a $user_info.
7024
	if (isset($user_info['id']) && $user_info['id'] == $userid && SMF != 'BACKGROUND')
7025
	{
7026
		$groups = $user_info['groups'];
7027
		$can_see_all_boards = $user_info['is_admin'] || $user_info['can_manage_boards'];
7028
		$ignoreboards = !empty($user_info['ignoreboards']) ? $user_info['ignoreboards'] : null;
7029
	}
7030
	else
7031
	{
7032
		$request = $smcFunc['db_query']('', '
7033
			SELECT mem.ignore_boards, mem.id_group, mem.additional_groups, mem.id_post_group
7034
			FROM {db_prefix}members AS mem
7035
			WHERE mem.id_member = {int:id_member}
7036
			LIMIT 1',
7037
			array(
7038
				'id_member' => $userid,
7039
			)
7040
		);
7041
7042
		$row = $smcFunc['db_fetch_assoc']($request);
7043
7044
		if (empty($row['additional_groups']))
7045
			$groups = array($row['id_group'], $row['id_post_group']);
7046
		else
7047
			$groups = array_merge(
7048
				array($row['id_group'], $row['id_post_group']),
7049
				explode(',', $row['additional_groups'])
7050
			);
7051
7052
		// Because history has proven that it is possible for groups to go bad - clean up in case.
7053
		foreach ($groups as $k => $v)
7054
			$groups[$k] = (int) $v;
7055
7056
		$can_see_all_boards = in_array(1, $groups) || (!empty($modSettings['board_manager_groups']) && count(array_intersect($groups, explode(',', $modSettings['board_manager_groups']))) > 0);
7057
7058
		$ignoreboards = !empty($row['ignore_boards']) && !empty($modSettings['allow_ignore_boards']) ? explode(',', $row['ignore_boards']) : array();
7059
	}
7060
7061
	// Just build this here, it makes it easier to change/use - administrators can see all boards.
7062
	if ($can_see_all_boards)
7063
		$query_part['query_see_board'] = '1=1';
7064
	// Otherwise just the groups in $user_info['groups'].
7065
	else
7066
	{
7067
		$query_part['query_see_board'] = '
7068
			EXISTS (
7069
				SELECT bpv.id_board
7070
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
7071
				WHERE bpv.id_group IN ('. implode(',', $groups) .')
7072
					AND bpv.deny = 0
7073
					AND bpv.id_board = b.id_board
7074
			)';
7075
7076
		if (!empty($modSettings['deny_boards_access']))
7077
			$query_part['query_see_board'] .= '
7078
			AND NOT EXISTS (
7079
				SELECT bpv.id_board
7080
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
7081
				WHERE bpv.id_group IN ( '. implode(',', $groups) .')
7082
					AND bpv.deny = 1
7083
					AND bpv.id_board = b.id_board
7084
			)';
7085
	}
7086
7087
	$query_part['query_see_message_board'] = str_replace('b.', 'm.', $query_part['query_see_board']);
7088
	$query_part['query_see_topic_board'] = str_replace('b.', 't.', $query_part['query_see_board']);
7089
7090
	// Build the list of boards they WANT to see.
7091
	// This will take the place of query_see_boards in certain spots, so it better include the boards they can see also
7092
7093
	// If they aren't ignoring any boards then they want to see all the boards they can see
7094
	if (empty($ignoreboards))
7095
	{
7096
		$query_part['query_wanna_see_board'] = $query_part['query_see_board'];
7097
		$query_part['query_wanna_see_message_board'] = $query_part['query_see_message_board'];
7098
		$query_part['query_wanna_see_topic_board'] = $query_part['query_see_topic_board'];
7099
	}
7100
	// Ok I guess they don't want to see all the boards
7101
	else
7102
	{
7103
		$query_part['query_wanna_see_board'] = '(' . $query_part['query_see_board'] . ' AND b.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7104
		$query_part['query_wanna_see_message_board'] = '(' . $query_part['query_see_message_board'] . ' AND m.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7105
		$query_part['query_wanna_see_topic_board'] = '(' . $query_part['query_see_topic_board'] . ' AND t.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7106
	}
7107
7108
	return $query_part;
7109
}
7110
7111
/**
7112
 * Check if the connection is using https.
7113
 *
7114
 * @return boolean true if connection used https
7115
 */
7116
function httpsOn()
7117
{
7118
	$secure = false;
7119
7120
	if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on')
7121
		$secure = true;
7122
	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...
7123
		$secure = true;
7124
7125
	return $secure;
7126
}
7127
7128
/**
7129
 * A wrapper for `filter_var($url, FILTER_VALIDATE_URL)` that can handle URLs
7130
 * with international characters (a.k.a. IRIs)
7131
 *
7132
 * @param string $iri The IRI to test.
7133
 * @param int $flags Optional flags to pass to filter_var()
7134
 * @return string|bool Either the original IRI, or false if the IRI was invalid.
7135
 */
7136
function validate_iri($iri, $flags = null)
7137
{
7138
	$url = iri_to_url($iri);
7139
7140
	if (filter_var($url, FILTER_VALIDATE_URL, $flags) !== false)
0 ignored issues
show
Bug introduced by
It seems like $flags can also be of type null; however, parameter $options of filter_var() does only seem to accept array|integer, maybe add an additional type check? ( Ignorable by Annotation )

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

7140
	if (filter_var($url, FILTER_VALIDATE_URL, /** @scrutinizer ignore-type */ $flags) !== false)
Loading history...
7141
		return $iri;
7142
	else
7143
		return false;
7144
}
7145
7146
/**
7147
 * A wrapper for `filter_var($url, FILTER_SANITIZE_URL)` that can handle URLs
7148
 * with international characters (a.k.a. IRIs)
7149
 *
7150
 * Note: The returned value will still be an IRI, not a URL. To convert to URL,
7151
 * feed the result of this function to iri_to_url()
7152
 *
7153
 * @param string $iri The IRI to sanitize.
7154
 * @return string|bool The sanitized version of the IRI
7155
 */
7156
function sanitize_iri($iri)
7157
{
7158
	// Encode any non-ASCII characters (but not space or control characters of any sort)
7159
	$iri = preg_replace_callback('~[^\x00-\x7F\pZ\pC]~u', function($matches)
7160
	{
7161
		return rawurlencode($matches[0]);
7162
	}, $iri);
7163
7164
	// Perform normal sanitization
7165
	$iri = filter_var($iri, FILTER_SANITIZE_URL);
7166
7167
	// Decode the non-ASCII characters
7168
	$iri = rawurldecode($iri);
7169
7170
	return $iri;
7171
}
7172
7173
/**
7174
 * Converts a URL with international characters (an IRI) into a pure ASCII URL
7175
 *
7176
 * Uses Punycode to encode any non-ASCII characters in the domain name, and uses
7177
 * standard URL encoding on the rest.
7178
 *
7179
 * @param string $iri A IRI that may or may not contain non-ASCII characters.
7180
 * @return string|bool The URL version of the IRI.
7181
 */
7182
function iri_to_url($iri)
7183
{
7184
	global $sourcedir;
7185
7186
	$host = parse_url((strpos($iri, '://') === false ? 'http://' : '') . ltrim($iri, ':/'), PHP_URL_HOST);
7187
7188
	if (empty($host))
7189
		return $iri;
7190
7191
	// Convert the domain using the Punycode algorithm
7192
	require_once($sourcedir . '/Class-Punycode.php');
7193
	$Punycode = new Punycode();
7194
	$encoded_host = $Punycode->encode($host);
7195
	$pos = strpos($iri, $host);
7196
	$iri = substr_replace($iri, $encoded_host, $pos, strlen($host));
7197
7198
	// Encode any disallowed characters in the rest of the URL
7199
	$unescaped = array(
7200
		'%21' => '!', '%23' => '#', '%24' => '$', '%26' => '&',
7201
		'%27' => "'", '%28' => '(', '%29' => ')', '%2A' => '*',
7202
		'%2B' => '+', '%2C' => ',', '%2F' => '/', '%3A' => ':',
7203
		'%3B' => ';', '%3D' => '=', '%3F' => '?', '%40' => '@',
7204
		'%25' => '%',
7205
	);
7206
	$iri = strtr(rawurlencode($iri), $unescaped);
0 ignored issues
show
Bug introduced by
It seems like $iri can also be of type array; however, parameter $string of rawurlencode() 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

7206
	$iri = strtr(rawurlencode(/** @scrutinizer ignore-type */ $iri), $unescaped);
Loading history...
7207
7208
	return $iri;
7209
}
7210
7211
/**
7212
 * Decodes a URL containing encoded international characters to UTF-8
7213
 *
7214
 * Decodes any Punycode encoded characters in the domain name, then uses
7215
 * standard URL decoding on the rest.
7216
 *
7217
 * @param string $url The pure ASCII version of a URL.
7218
 * @return string|bool The UTF-8 version of the URL.
7219
 */
7220
function url_to_iri($url)
7221
{
7222
	global $sourcedir;
7223
7224
	$host = parse_url((strpos($url, '://') === false ? 'http://' : '') . ltrim($url, ':/'), PHP_URL_HOST);
7225
7226
	if (empty($host))
7227
		return $url;
7228
7229
	// Decode the domain from Punycode
7230
	require_once($sourcedir . '/Class-Punycode.php');
7231
	$Punycode = new Punycode();
7232
	$decoded_host = $Punycode->decode($host);
7233
	$pos = strpos($url, $host);
7234
	$url = substr_replace($url, $decoded_host, $pos, strlen($host));
7235
7236
	// Decode the rest of the URL
7237
	$url = rawurldecode($url);
0 ignored issues
show
Bug introduced by
It seems like $url can also be of type array; however, parameter $string of rawurldecode() 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

7237
	$url = rawurldecode(/** @scrutinizer ignore-type */ $url);
Loading history...
7238
7239
	return $url;
7240
}
7241
7242
/**
7243
 * Ensures SMF's scheduled tasks are being run as intended
7244
 *
7245
 * If the admin activated the cron_is_real_cron setting, but the cron job is
7246
 * not running things at least once per day, we need to go back to SMF's default
7247
 * behaviour using "web cron" JavaScript calls.
7248
 */
7249
function check_cron()
7250
{
7251
	global $modSettings, $smcFunc, $txt;
7252
7253
	if (!empty($modSettings['cron_is_real_cron']) && time() - @intval($modSettings['cron_last_checked']) > 84600)
7254
	{
7255
		$request = $smcFunc['db_query']('', '
7256
			SELECT COUNT(*)
7257
			FROM {db_prefix}scheduled_tasks
7258
			WHERE disabled = {int:not_disabled}
7259
				AND next_time < {int:yesterday}',
7260
			array(
7261
				'not_disabled' => 0,
7262
				'yesterday' => time() - 84600,
7263
			)
7264
		);
7265
		list($overdue) = $smcFunc['db_fetch_row']($request);
7266
		$smcFunc['db_free_result']($request);
7267
7268
		// If we have tasks more than a day overdue, cron isn't doing its job.
7269
		if (!empty($overdue))
7270
		{
7271
			loadLanguage('ManageScheduledTasks');
7272
			log_error($txt['cron_not_working']);
7273
			updateSettings(array('cron_is_real_cron' => 0));
7274
		}
7275
		else
7276
			updateSettings(array('cron_last_checked' => time()));
7277
	}
7278
}
7279
7280
/**
7281
 * Sends an appropriate HTTP status header based on a given status code
7282
 *
7283
 * @param int $code The status code
7284
 * @param string $status The string for the status. Set automatically if not provided.
7285
 */
7286
function send_http_status($code, $status = '')
7287
{
7288
	$statuses = array(
7289
		206 => 'Partial Content',
7290
		304 => 'Not Modified',
7291
		400 => 'Bad Request',
7292
		403 => 'Forbidden',
7293
		404 => 'Not Found',
7294
		410 => 'Gone',
7295
		500 => 'Internal Server Error',
7296
		503 => 'Service Unavailable',
7297
	);
7298
7299
	$protocol = preg_match('~^\s*(HTTP/[12]\.\d)\s*$~i', $_SERVER['SERVER_PROTOCOL'], $matches) ? $matches[1] : 'HTTP/1.0';
7300
7301
	if (!isset($statuses[$code]) && empty($status))
7302
		header($protocol . ' 500 Internal Server Error');
7303
	else
7304
		header($protocol . ' ' . $code . ' ' . (!empty($status) ? $status : $statuses[$code]));
7305
}
7306
7307
/**
7308
 * Concatenates an array of strings into a grammatically correct sentence list
7309
 *
7310
 * Uses formats defined in the language files to build the list appropropriately
7311
 * for the currently loaded language.
7312
 *
7313
 * @param array $list An array of strings to concatenate.
7314
 * @return string The localized sentence list.
7315
 */
7316
function sentence_list($list)
7317
{
7318
	global $txt;
7319
7320
	// Make sure the bare necessities are defined
7321
	if (empty($txt['sentence_list_format']['n']))
7322
		$txt['sentence_list_format']['n'] = '{series}';
7323
	if (!isset($txt['sentence_list_separator']))
7324
		$txt['sentence_list_separator'] = ', ';
7325
	if (!isset($txt['sentence_list_separator_alt']))
7326
		$txt['sentence_list_separator_alt'] = '; ';
7327
7328
	// Which format should we use?
7329
	if (isset($txt['sentence_list_format'][count($list)]))
7330
		$format = $txt['sentence_list_format'][count($list)];
7331
	else
7332
		$format = $txt['sentence_list_format']['n'];
7333
7334
	// Do we want the normal separator or the alternate?
7335
	$separator = $txt['sentence_list_separator'];
7336
	foreach ($list as $item)
7337
	{
7338
		if (strpos($item, $separator) !== false)
7339
		{
7340
			$separator = $txt['sentence_list_separator_alt'];
7341
			$format = strtr($format, trim($txt['sentence_list_separator']), trim($separator));
7342
			break;
7343
		}
7344
	}
7345
7346
	$replacements = array();
7347
7348
	// Special handling for the last items on the list
7349
	$i = 0;
7350
	while (empty($done))
7351
	{
7352
		if (strpos($format, '{'. --$i . '}') !== false)
7353
			$replacements['{'. $i . '}'] = array_pop($list);
7354
		else
7355
			$done = true;
7356
	}
7357
	unset($done);
7358
7359
	// Special handling for the first items on the list
7360
	$i = 0;
7361
	while (empty($done))
7362
	{
7363
		if (strpos($format, '{'. ++$i . '}') !== false)
7364
			$replacements['{'. $i . '}'] = array_shift($list);
7365
		else
7366
			$done = true;
7367
	}
7368
	unset($done);
7369
7370
	// Whatever is left
7371
	$replacements['{series}'] = implode($separator, $list);
7372
7373
	// Do the deed
7374
	return strtr($format, $replacements);
7375
}
7376
7377
/**
7378
 * Truncate an array to a specified length
7379
 *
7380
 * @param array $array The array to truncate
7381
 * @param int $max_length The upperbound on the length
7382
 * @param int $deep How levels in an multidimensional array should the function take into account.
7383
 * @return array The truncated array
7384
 */
7385
function truncate_array($array, $max_length = 1900, $deep = 3)
7386
{
7387
	$array = (array) $array;
7388
7389
	$curr_length = array_length($array, $deep);
7390
7391
	if ($curr_length <= $max_length)
7392
		return $array;
7393
7394
	else
7395
	{
7396
		// Truncate each element's value to a reasonable length
7397
		$param_max = floor($max_length / count($array));
7398
7399
		$current_deep = $deep - 1;
7400
7401
		foreach ($array as $key => &$value)
7402
		{
7403
			if (is_array($value))
7404
				if ($current_deep > 0)
7405
					$value = truncate_array($value, $current_deep);
7406
7407
			else
7408
				$value = substr($value, 0, $param_max - strlen($key) - 5);
0 ignored issues
show
Bug introduced by
$param_max - strlen($key) - 5 of type double is incompatible with the type integer|null expected by parameter $length of substr(). ( Ignorable by Annotation )

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

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

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

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