Passed
Push — release-2.1 ( 229f76...d172d9 )
by Mathias
35s queued 11s
created

get_date_or_time_format()   B

Complexity

Conditions 10
Paths 70

Size

Total Lines 111
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 55
nc 70
nop 2
dl 0
loc 111
rs 7.1151
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

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

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

1500
						for ($php_i = 0, $php_n = count(/** @scrutinizer ignore-type */ $php_parts); $php_i < $php_n; $php_i++)
Loading history...
1501
						{
1502
							// Do PHP code coloring?
1503
							if ($php_parts[$php_i] != '&lt;?php')
1504
								continue;
1505
1506
							$php_string = '';
1507
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1508
							{
1509
								$php_string .= $php_parts[$php_i];
1510
								$php_parts[$php_i++] = '';
1511
							}
1512
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1513
						}
1514
1515
						// Fix the PHP code stuff...
1516
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
0 ignored issues
show
Bug introduced by
It seems like $php_parts can also be of type false; however, parameter $pieces of implode() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

1516
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', /** @scrutinizer ignore-type */ $php_parts));
Loading history...
1517
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1518
1519
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1520
						if (!empty($context['browser']['is_opera']))
1521
							$data .= '&nbsp;';
1522
					}
1523
				},
1524
				'block_level' => true,
1525
			),
1526
			array(
1527
				'tag' => 'code',
1528
				'type' => 'unparsed_equals_content',
1529
				'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>',
1530
				// @todo Maybe this can be simplified?
1531
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1532
				{
1533
					if (!isset($disabled['code']))
1534
					{
1535
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1536
1537
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
0 ignored issues
show
Bug introduced by
It seems like $php_parts can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

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

1537
						for ($php_i = 0, $php_n = count(/** @scrutinizer ignore-type */ $php_parts); $php_i < $php_n; $php_i++)
Loading history...
1538
						{
1539
							// Do PHP code coloring?
1540
							if ($php_parts[$php_i] != '&lt;?php')
1541
								continue;
1542
1543
							$php_string = '';
1544
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1545
							{
1546
								$php_string .= $php_parts[$php_i];
1547
								$php_parts[$php_i++] = '';
1548
							}
1549
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1550
						}
1551
1552
						// Fix the PHP code stuff...
1553
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
0 ignored issues
show
Bug introduced by
It seems like $php_parts can also be of type false; however, parameter $pieces of implode() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

1553
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', /** @scrutinizer ignore-type */ $php_parts));
Loading history...
1554
						$data[0] = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data[0]);
1555
1556
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1557
						if (!empty($context['browser']['is_opera']))
1558
							$data[0] .= '&nbsp;';
1559
					}
1560
				},
1561
				'block_level' => true,
1562
			),
1563
			array(
1564
				'tag' => 'color',
1565
				'type' => 'unparsed_equals',
1566
				'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]?)\))\]',
1567
				'before' => '<span style="color: $1;" class="bbc_color">',
1568
				'after' => '</span>',
1569
			),
1570
			array(
1571
				'tag' => 'email',
1572
				'type' => 'unparsed_content',
1573
				'content' => '<a href="mailto:$1" class="bbc_email">$1</a>',
1574
				// @todo Should this respect guest_hideContacts?
1575
				'validate' => function(&$tag, &$data, $disabled)
1576
				{
1577
					$data = strtr($data, array('<br>' => ''));
1578
				},
1579
			),
1580
			array(
1581
				'tag' => 'email',
1582
				'type' => 'unparsed_equals',
1583
				'before' => '<a href="mailto:$1" class="bbc_email">',
1584
				'after' => '</a>',
1585
				// @todo Should this respect guest_hideContacts?
1586
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1587
				'disabled_after' => ' ($1)',
1588
			),
1589
			// Legacy (and just a link even when not disabled)
1590
			array(
1591
				'tag' => 'flash',
1592
				'type' => 'unparsed_commas_content',
1593
				'test' => '\d+,\d+\]',
1594
				'content' => '<a href="$1" target="_blank" rel="noopener">$1</a>',
1595
				'validate' => function (&$tag, &$data, $disabled)
1596
				{
1597
					$scheme = parse_url($data[0], PHP_URL_SCHEME);
1598
					if (empty($scheme))
1599
						$data[0] = '//' . ltrim($data[0], ':/');
1600
				},
1601
			),
1602
			array(
1603
				'tag' => 'float',
1604
				'type' => 'unparsed_equals',
1605
				'test' => '(left|right)(\s+max=\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)?\]',
1606
				'before' => '<div $1>',
1607
				'after' => '</div>',
1608
				'validate' => function(&$tag, &$data, $disabled)
1609
				{
1610
					$class = 'class="bbc_float float' . (strpos($data, 'left') === 0 ? 'left' : 'right') . '"';
1611
1612
					if (preg_match('~\bmax=(\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)~', $data, $matches))
1613
						$css = ' style="max-width:' . $matches[1] . (is_numeric($matches[1]) ? 'px' : '') . '"';
1614
					else
1615
						$css = '';
1616
1617
					$data = $class . $css;
1618
				},
1619
				'trim' => 'outside',
1620
				'block_level' => true,
1621
			),
1622
			// Legacy (alias of [url] with an FTP URL)
1623
			array(
1624
				'tag' => 'ftp',
1625
				'type' => 'unparsed_content',
1626
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
1627
				'validate' => function(&$tag, &$data, $disabled)
1628
				{
1629
					$data = strtr($data, array('<br>' => ''));
1630
					$scheme = parse_url($data, PHP_URL_SCHEME);
1631
					if (empty($scheme))
1632
						$data = 'ftp://' . ltrim($data, ':/');
1633
				},
1634
			),
1635
			// Legacy (alias of [url] with an FTP URL)
1636
			array(
1637
				'tag' => 'ftp',
1638
				'type' => 'unparsed_equals',
1639
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
1640
				'after' => '</a>',
1641
				'validate' => function(&$tag, &$data, $disabled)
1642
				{
1643
					$scheme = parse_url($data, PHP_URL_SCHEME);
1644
					if (empty($scheme))
1645
						$data = 'ftp://' . ltrim($data, ':/');
1646
				},
1647
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1648
				'disabled_after' => ' ($1)',
1649
			),
1650
			array(
1651
				'tag' => 'font',
1652
				'type' => 'unparsed_equals',
1653
				'test' => '[A-Za-z0-9_,\-\s]+?\]',
1654
				'before' => '<span style="font-family: $1;" class="bbc_font">',
1655
				'after' => '</span>',
1656
			),
1657
			// Legacy (one of those things that should not be done)
1658
			array(
1659
				'tag' => 'glow',
1660
				'type' => 'unparsed_commas',
1661
				'test' => '[#0-9a-zA-Z\-]{3,12},([012]\d{1,2}|\d{1,2})(,[^]]+)?\]',
1662
				'before' => '<span style="text-shadow: $1 1px 1px 1px">',
1663
				'after' => '</span>',
1664
			),
1665
			// Legacy (alias of [color=green])
1666
			array(
1667
				'tag' => 'green',
1668
				'before' => '<span style="color: green;" class="bbc_color">',
1669
				'after' => '</span>',
1670
			),
1671
			array(
1672
				'tag' => 'html',
1673
				'type' => 'unparsed_content',
1674
				'content' => '<div>$1</div>',
1675
				'block_level' => true,
1676
				'disabled_content' => '$1',
1677
			),
1678
			array(
1679
				'tag' => 'hr',
1680
				'type' => 'closed',
1681
				'content' => '<hr>',
1682
				'block_level' => true,
1683
			),
1684
			array(
1685
				'tag' => 'i',
1686
				'before' => '<i>',
1687
				'after' => '</i>',
1688
			),
1689
			array(
1690
				'tag' => 'img',
1691
				'type' => 'unparsed_content',
1692
				'parameters' => array(
1693
					'alt' => array('optional' => true),
1694
					'title' => array('optional' => true),
1695
					'width' => array('optional' => true, 'value' => ' width="$1"', 'match' => '(\d+)'),
1696
					'height' => array('optional' => true, 'value' => ' height="$1"', 'match' => '(\d+)'),
1697
				),
1698
				'content' => '<img src="$1" alt="{alt}" title="{title}"{width}{height} class="bbc_img resized">',
1699
				'validate' => function(&$tag, &$data, $disabled)
1700
				{
1701
					global $image_proxy_enabled, $user_info;
1702
1703
					$data = strtr($data, array('<br>' => ''));
1704
					$scheme = parse_url($data, PHP_URL_SCHEME);
1705
					if ($image_proxy_enabled)
1706
					{
1707
						if (!empty($user_info['possibly_robot']))
1708
							return;
1709
1710
						if (empty($scheme))
1711
							$data = 'http://' . ltrim($data, ':/');
1712
1713
						if ($scheme != 'https')
1714
							$data = get_proxied_url($data);
1715
					}
1716
					elseif (empty($scheme))
1717
						$data = '//' . ltrim($data, ':/');
1718
				},
1719
				'disabled_content' => '($1)',
1720
			),
1721
			array(
1722
				'tag' => 'img',
1723
				'type' => 'unparsed_content',
1724
				'content' => '<img src="$1" alt="" class="bbc_img">',
1725
				'validate' => function(&$tag, &$data, $disabled)
1726
				{
1727
					global $image_proxy_enabled, $user_info;
1728
1729
					$data = strtr($data, array('<br>' => ''));
1730
					$scheme = parse_url($data, PHP_URL_SCHEME);
1731
					if ($image_proxy_enabled)
1732
					{
1733
						if (!empty($user_info['possibly_robot']))
1734
							return;
1735
1736
						if (empty($scheme))
1737
							$data = 'http://' . ltrim($data, ':/');
1738
1739
						if ($scheme != 'https')
1740
							$data = get_proxied_url($data);
1741
					}
1742
					elseif (empty($scheme))
1743
						$data = '//' . ltrim($data, ':/');
1744
				},
1745
				'disabled_content' => '($1)',
1746
			),
1747
			array(
1748
				'tag' => 'iurl',
1749
				'type' => 'unparsed_content',
1750
				'content' => '<a href="$1" class="bbc_link">$1</a>',
1751
				'validate' => function(&$tag, &$data, $disabled)
1752
				{
1753
					$data = strtr($data, array('<br>' => ''));
1754
					$scheme = parse_url($data, PHP_URL_SCHEME);
1755
					if (empty($scheme))
1756
						$data = '//' . ltrim($data, ':/');
1757
				},
1758
			),
1759
			array(
1760
				'tag' => 'iurl',
1761
				'type' => 'unparsed_equals',
1762
				'quoted' => 'optional',
1763
				'before' => '<a href="$1" class="bbc_link">',
1764
				'after' => '</a>',
1765
				'validate' => function(&$tag, &$data, $disabled)
1766
				{
1767
					if (substr($data, 0, 1) == '#')
1768
						$data = '#post_' . substr($data, 1);
1769
					else
1770
					{
1771
						$scheme = parse_url($data, PHP_URL_SCHEME);
1772
						if (empty($scheme))
1773
							$data = '//' . ltrim($data, ':/');
1774
					}
1775
				},
1776
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1777
				'disabled_after' => ' ($1)',
1778
			),
1779
			array(
1780
				'tag' => 'justify',
1781
				'before' => '<div style="text-align: justify;">',
1782
				'after' => '</div>',
1783
				'block_level' => true,
1784
			),
1785
			array(
1786
				'tag' => 'left',
1787
				'before' => '<div style="text-align: left;">',
1788
				'after' => '</div>',
1789
				'block_level' => true,
1790
			),
1791
			array(
1792
				'tag' => 'li',
1793
				'before' => '<li>',
1794
				'after' => '</li>',
1795
				'trim' => 'outside',
1796
				'require_parents' => array('list'),
1797
				'block_level' => true,
1798
				'disabled_before' => '',
1799
				'disabled_after' => '<br>',
1800
			),
1801
			array(
1802
				'tag' => 'list',
1803
				'before' => '<ul class="bbc_list">',
1804
				'after' => '</ul>',
1805
				'trim' => 'inside',
1806
				'require_children' => array('li', 'list'),
1807
				'block_level' => true,
1808
			),
1809
			array(
1810
				'tag' => 'list',
1811
				'parameters' => array(
1812
					'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)'),
1813
				),
1814
				'before' => '<ul class="bbc_list" style="list-style-type: {type};">',
1815
				'after' => '</ul>',
1816
				'trim' => 'inside',
1817
				'require_children' => array('li'),
1818
				'block_level' => true,
1819
			),
1820
			array(
1821
				'tag' => 'ltr',
1822
				'before' => '<bdo dir="ltr">',
1823
				'after' => '</bdo>',
1824
				'block_level' => true,
1825
			),
1826
			array(
1827
				'tag' => 'me',
1828
				'type' => 'unparsed_equals',
1829
				'before' => '<div class="meaction">* $1 ',
1830
				'after' => '</div>',
1831
				'quoted' => 'optional',
1832
				'block_level' => true,
1833
				'disabled_before' => '/me ',
1834
				'disabled_after' => '<br>',
1835
			),
1836
			array(
1837
				'tag' => 'member',
1838
				'type' => 'unparsed_equals',
1839
				'before' => '<a href="' . $scripturl . '?action=profile;u=$1" class="mention" data-mention="$1">@',
1840
				'after' => '</a>',
1841
			),
1842
			// Legacy (horrible memories of the 1990s)
1843
			array(
1844
				'tag' => 'move',
1845
				'before' => '<marquee>',
1846
				'after' => '</marquee>',
1847
				'block_level' => true,
1848
				'disallow_children' => array('move'),
1849
			),
1850
			array(
1851
				'tag' => 'nobbc',
1852
				'type' => 'unparsed_content',
1853
				'content' => '$1',
1854
			),
1855
			array(
1856
				'tag' => 'php',
1857
				'type' => 'unparsed_content',
1858
				'content' => '<span class="phpcode">$1</span>',
1859
				'validate' => isset($disabled['php']) ? null : function(&$tag, &$data, $disabled)
1860
				{
1861
					if (!isset($disabled['php']))
1862
					{
1863
						$add_begin = substr(trim($data), 0, 5) != '&lt;?';
1864
						$data = highlight_php_code($add_begin ? '&lt;?php ' . $data . '?&gt;' : $data);
1865
						if ($add_begin)
1866
							$data = preg_replace(array('~^(.+?)&lt;\?.{0,40}?php(?:&nbsp;|\s)~', '~\?&gt;((?:</(font|span)>)*)$~'), '$1', $data, 2);
1867
					}
1868
				},
1869
				'block_level' => false,
1870
				'disabled_content' => '$1',
1871
			),
1872
			array(
1873
				'tag' => 'pre',
1874
				'before' => '<pre>',
1875
				'after' => '</pre>',
1876
			),
1877
			array(
1878
				'tag' => 'quote',
1879
				'before' => '<blockquote><cite>' . $txt['quote'] . '</cite>',
1880
				'after' => '</blockquote>',
1881
				'trim' => 'both',
1882
				'block_level' => true,
1883
			),
1884
			array(
1885
				'tag' => 'quote',
1886
				'parameters' => array(
1887
					'author' => array('match' => '(.{1,192}?)', 'quoted' => true),
1888
				),
1889
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1890
				'after' => '</blockquote>',
1891
				'trim' => 'both',
1892
				'block_level' => true,
1893
			),
1894
			array(
1895
				'tag' => 'quote',
1896
				'type' => 'parsed_equals',
1897
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': $1</cite>',
1898
				'after' => '</blockquote>',
1899
				'trim' => 'both',
1900
				'quoted' => 'optional',
1901
				// Don't allow everything to be embedded with the author name.
1902
				'parsed_tags_allowed' => array('url', 'iurl', 'ftp'),
1903
				'block_level' => true,
1904
			),
1905
			array(
1906
				'tag' => 'quote',
1907
				'parameters' => array(
1908
					'author' => array('match' => '([^<>]{1,192}?)'),
1909
					'link' => array('match' => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|msg=\d+?|action=profile;u=\d+)'),
1910
					'date' => array('match' => '(\d+)', 'validate' => 'timeformat'),
1911
				),
1912
				'before' => '<blockquote><cite><a href="' . $scripturl . '?{link}">' . $txt['quote_from'] . ': {author} ' . $txt['search_on'] . ' {date}</a></cite>',
1913
				'after' => '</blockquote>',
1914
				'trim' => 'both',
1915
				'block_level' => true,
1916
			),
1917
			array(
1918
				'tag' => 'quote',
1919
				'parameters' => array(
1920
					'author' => array('match' => '(.{1,192}?)'),
1921
				),
1922
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1923
				'after' => '</blockquote>',
1924
				'trim' => 'both',
1925
				'block_level' => true,
1926
			),
1927
			// Legacy (alias of [color=red])
1928
			array(
1929
				'tag' => 'red',
1930
				'before' => '<span style="color: red;" class="bbc_color">',
1931
				'after' => '</span>',
1932
			),
1933
			array(
1934
				'tag' => 'right',
1935
				'before' => '<div style="text-align: right;">',
1936
				'after' => '</div>',
1937
				'block_level' => true,
1938
			),
1939
			array(
1940
				'tag' => 'rtl',
1941
				'before' => '<bdo dir="rtl">',
1942
				'after' => '</bdo>',
1943
				'block_level' => true,
1944
			),
1945
			array(
1946
				'tag' => 's',
1947
				'before' => '<s>',
1948
				'after' => '</s>',
1949
			),
1950
			// Legacy (never a good idea)
1951
			array(
1952
				'tag' => 'shadow',
1953
				'type' => 'unparsed_commas',
1954
				'test' => '[#0-9a-zA-Z\-]{3,12},(left|right|top|bottom|[0123]\d{0,2})\]',
1955
				'before' => '<span style="text-shadow: $1 $2">',
1956
				'after' => '</span>',
1957
				'validate' => function(&$tag, &$data, $disabled)
1958
				{
1959
1960
					if ($data[1] == 'top' || (is_numeric($data[1]) && $data[1] < 50))
1961
						$data[1] = '0 -2px 1px';
1962
1963
					elseif ($data[1] == 'right' || (is_numeric($data[1]) && $data[1] < 100))
1964
						$data[1] = '2px 0 1px';
1965
1966
					elseif ($data[1] == 'bottom' || (is_numeric($data[1]) && $data[1] < 190))
1967
						$data[1] = '0 2px 1px';
1968
1969
					elseif ($data[1] == 'left' || (is_numeric($data[1]) && $data[1] < 280))
1970
						$data[1] = '-2px 0 1px';
1971
1972
					else
1973
						$data[1] = '1px 1px 1px';
1974
				},
1975
			),
1976
			array(
1977
				'tag' => 'size',
1978
				'type' => 'unparsed_equals',
1979
				'test' => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)\]',
1980
				'before' => '<span style="font-size: $1;" class="bbc_size">',
1981
				'after' => '</span>',
1982
			),
1983
			array(
1984
				'tag' => 'size',
1985
				'type' => 'unparsed_equals',
1986
				'test' => '[1-7]\]',
1987
				'before' => '<span style="font-size: $1;" class="bbc_size">',
1988
				'after' => '</span>',
1989
				'validate' => function(&$tag, &$data, $disabled)
1990
				{
1991
					$sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95);
1992
					$data = $sizes[$data] . 'em';
1993
				},
1994
			),
1995
			array(
1996
				'tag' => 'sub',
1997
				'before' => '<sub>',
1998
				'after' => '</sub>',
1999
			),
2000
			array(
2001
				'tag' => 'sup',
2002
				'before' => '<sup>',
2003
				'after' => '</sup>',
2004
			),
2005
			array(
2006
				'tag' => 'table',
2007
				'before' => '<table class="bbc_table">',
2008
				'after' => '</table>',
2009
				'trim' => 'inside',
2010
				'require_children' => array('tr'),
2011
				'block_level' => true,
2012
			),
2013
			array(
2014
				'tag' => 'td',
2015
				'before' => '<td>',
2016
				'after' => '</td>',
2017
				'require_parents' => array('tr'),
2018
				'trim' => 'outside',
2019
				'block_level' => true,
2020
				'disabled_before' => '',
2021
				'disabled_after' => '',
2022
			),
2023
			array(
2024
				'tag' => 'time',
2025
				'type' => 'unparsed_content',
2026
				'content' => '$1',
2027
				'validate' => function(&$tag, &$data, $disabled)
2028
				{
2029
					if (is_numeric($data))
2030
						$data = timeformat($data);
2031
					else
2032
						$tag['content'] = '[time]$1[/time]';
2033
				},
2034
			),
2035
			array(
2036
				'tag' => 'tr',
2037
				'before' => '<tr>',
2038
				'after' => '</tr>',
2039
				'require_parents' => array('table'),
2040
				'require_children' => array('td'),
2041
				'trim' => 'both',
2042
				'block_level' => true,
2043
				'disabled_before' => '',
2044
				'disabled_after' => '',
2045
			),
2046
			// Legacy (the <tt> element is dead)
2047
			array(
2048
				'tag' => 'tt',
2049
				'before' => '<span class="monospace">',
2050
				'after' => '</span>',
2051
			),
2052
			array(
2053
				'tag' => 'u',
2054
				'before' => '<u>',
2055
				'after' => '</u>',
2056
			),
2057
			array(
2058
				'tag' => 'url',
2059
				'type' => 'unparsed_content',
2060
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
2061
				'validate' => function(&$tag, &$data, $disabled)
2062
				{
2063
					$data = strtr($data, array('<br>' => ''));
2064
					$scheme = parse_url($data, PHP_URL_SCHEME);
2065
					if (empty($scheme))
2066
						$data = '//' . ltrim($data, ':/');
2067
				},
2068
			),
2069
			array(
2070
				'tag' => 'url',
2071
				'type' => 'unparsed_equals',
2072
				'quoted' => 'optional',
2073
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
2074
				'after' => '</a>',
2075
				'validate' => function(&$tag, &$data, $disabled)
2076
				{
2077
					$scheme = parse_url($data, PHP_URL_SCHEME);
2078
					if (empty($scheme))
2079
						$data = '//' . ltrim($data, ':/');
2080
				},
2081
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
2082
				'disabled_after' => ' ($1)',
2083
			),
2084
			// Legacy (alias of [color=white])
2085
			array(
2086
				'tag' => 'white',
2087
				'before' => '<span style="color: white;" class="bbc_color">',
2088
				'after' => '</span>',
2089
			),
2090
			array(
2091
				'tag' => 'youtube',
2092
				'type' => 'unparsed_content',
2093
				'content' => '<div class="videocontainer"><div><iframe frameborder="0" src="https://www.youtube.com/embed/$1?origin=' . $hosturl . '&wmode=opaque" data-youtube-id="$1" allowfullscreen></iframe></div></div>',
2094
				'disabled_content' => '<a href="https://www.youtube.com/watch?v=$1" target="_blank" rel="noopener">https://www.youtube.com/watch?v=$1</a>',
2095
				'block_level' => true,
2096
			),
2097
		);
2098
2099
		// Inside these tags autolink is not recommendable.
2100
		$no_autolink_tags = array(
2101
			'url',
2102
			'iurl',
2103
			'email',
2104
			'img',
2105
			'html',
2106
		);
2107
2108
		// Let mods add new BBC without hassle.
2109
		call_integration_hook('integrate_bbc_codes', array(&$codes, &$no_autolink_tags));
2110
2111
		// This is mainly for the bbc manager, so it's easy to add tags above.  Custom BBC should be added above this line.
2112
		if ($message === false)
0 ignored issues
show
introduced by
The condition $message === false is always false.
Loading history...
2113
		{
2114
			usort($codes, function($a, $b)
2115
			{
2116
				return strcmp($a['tag'], $b['tag']);
2117
			});
2118
			return $codes;
2119
		}
2120
2121
		// So the parser won't skip them.
2122
		$itemcodes = array(
2123
			'*' => 'disc',
2124
			'@' => 'disc',
2125
			'+' => 'square',
2126
			'x' => 'square',
2127
			'#' => 'square',
2128
			'o' => 'circle',
2129
			'O' => 'circle',
2130
			'0' => 'circle',
2131
		);
2132
		if (!isset($disabled['li']) && !isset($disabled['list']))
2133
		{
2134
			foreach ($itemcodes as $c => $dummy)
2135
				$bbc_codes[$c] = array();
2136
		}
2137
2138
		// Shhhh!
2139
		if (!isset($disabled['color']))
2140
		{
2141
			$codes[] = array(
2142
				'tag' => 'chrissy',
2143
				'before' => '<span style="color: #cc0099;">',
2144
				'after' => ' :-*</span>',
2145
			);
2146
			$codes[] = array(
2147
				'tag' => 'kissy',
2148
				'before' => '<span style="color: #cc0099;">',
2149
				'after' => ' :-*</span>',
2150
			);
2151
		}
2152
		$codes[] = array(
2153
			'tag' => 'cowsay',
2154
			'parameters' => array(
2155
				'e' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => 'oo', 'validate' => function ($eyes) use ($smcFunc)
2156
					{
2157
						static $css_added;
2158
2159
						if (empty($css_added))
2160
						{
2161
							$css = base64_decode('cHJlW2RhdGEtZV1bZGF0YS10XXt3aGl0ZS1zcGFjZTpwcmUtd3JhcDtsaW5lLWhlaWdodDppbml0aWFsO31wcmVbZGF0YS1lXVtkYXRhLXRdID4gZGl2e2Rpc3BsYXk6dGFibGU7Ym9yZGVyOjFweCBzb2xpZDtib3JkZXItcmFkaXVzOjAuNWVtO3BhZGRpbmc6MWNoO21heC13aWR0aDo4MGNoO21pbi13aWR0aDoxMmNoO31wcmVbZGF0YS1lXVtkYXRhLXRdOjphZnRlcntkaXNwbGF5OmlubGluZS1ibG9jazttYXJnaW4tbGVmdDo4Y2g7bWluLXdpZHRoOjIwY2g7ZGlyZWN0aW9uOmx0cjtjb250ZW50OidcNUMgICBeX19eXEEgIFw1QyAgKCcgYXR0cihkYXRhLWUpICcpXDVDX19fX19fX1xBICAgIChfXylcNUMgICAgICAgIClcNUMvXDVDXEEgICAgICcgYXR0cihkYXRhLXQpICcgfHwtLS0tdyB8XEEgICAgICAgIHx8ICAgICB8fCc7fQ==');
2162
2163
							addInlineJavaScript('
2164
								$("head").append("<style>" + ' . JavaScriptEscape($css) . ' + "</style>");', true);
2165
2166
							$css_added = true;
2167
						}
2168
2169
						return $smcFunc['substr']($eyes . 'oo', 0, 2);
2170
					},
2171
				),
2172
				't' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => '  ', 'validate' => function ($tongue) use ($smcFunc)
2173
					{
2174
						return $smcFunc['substr']($tongue . '  ', 0, 2);
2175
					},
2176
				),
2177
			),
2178
			'before' => '<pre data-e="{e}" data-t="{t}"><div>',
2179
			'after' => '</div></pre>',
2180
			'block_level' => true,
2181
		);
2182
2183
		foreach ($codes as $code)
2184
		{
2185
			// Make it easier to process parameters later
2186
			if (!empty($code['parameters']))
2187
				ksort($code['parameters'], SORT_STRING);
2188
2189
			// If we are not doing every tag only do ones we are interested in.
2190
			if (empty($parse_tags) || in_array($code['tag'], $parse_tags))
2191
				$bbc_codes[substr($code['tag'], 0, 1)][] = $code;
2192
		}
2193
		$codes = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $codes is dead and can be removed.
Loading history...
2194
	}
2195
2196
	// Shall we take the time to cache this?
2197
	if ($cache_id != '' && !empty($modSettings['cache_enable']) && (($modSettings['cache_enable'] >= 2 && isset($message[1000])) || isset($message[2400])) && empty($parse_tags))
2198
	{
2199
		// It's likely this will change if the message is modified.
2200
		$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']);
2201
2202
		if (($temp = cache_get_data($cache_key, 240)) != null)
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $temp = cache_get_data($cache_key, 240) of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
2203
			return $temp;
2204
2205
		$cache_t = microtime(true);
2206
	}
2207
2208
	if ($smileys === 'print')
0 ignored issues
show
introduced by
The condition $smileys === 'print' is always false.
Loading history...
2209
	{
2210
		// [glow], [shadow], and [move] can't really be printed.
2211
		$disabled['glow'] = true;
2212
		$disabled['shadow'] = true;
2213
		$disabled['move'] = true;
2214
2215
		// Colors can't well be displayed... supposed to be black and white.
2216
		$disabled['color'] = true;
2217
		$disabled['black'] = true;
2218
		$disabled['blue'] = true;
2219
		$disabled['white'] = true;
2220
		$disabled['red'] = true;
2221
		$disabled['green'] = true;
2222
		$disabled['me'] = true;
2223
2224
		// Color coding doesn't make sense.
2225
		$disabled['php'] = true;
2226
2227
		// Links are useless on paper... just show the link.
2228
		$disabled['ftp'] = true;
2229
		$disabled['url'] = true;
2230
		$disabled['iurl'] = true;
2231
		$disabled['email'] = true;
2232
		$disabled['flash'] = true;
2233
2234
		// @todo Change maybe?
2235
		if (!isset($_GET['images']))
2236
			$disabled['img'] = true;
2237
2238
		// Maybe some custom BBC need to be disabled for printing.
2239
		call_integration_hook('integrate_bbc_print', array(&$disabled));
2240
	}
2241
2242
	$open_tags = array();
2243
	$message = strtr($message, array("\n" => '<br>'));
2244
2245
	if (!empty($parse_tags))
2246
	{
2247
		$real_alltags_regex = $alltags_regex;
2248
		$alltags_regex = '';
2249
	}
2250
	if (empty($alltags_regex))
2251
	{
2252
		$alltags = array();
2253
		foreach ($bbc_codes as $section)
2254
		{
2255
			foreach ($section as $code)
2256
				$alltags[] = $code['tag'];
2257
		}
2258
		$alltags_regex = '(?' . '>\b' . build_regex(array_unique($alltags)) . '\b|' . build_regex(array_keys($itemcodes)) . ')';
2259
	}
2260
2261
	$pos = -1;
2262
	while ($pos !== false)
2263
	{
2264
		$last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos;
2265
		preg_match('~\[/?(?=' . $alltags_regex . ')~i', $message, $matches, PREG_OFFSET_CAPTURE, $pos + 1);
2266
		$pos = isset($matches[0][1]) ? $matches[0][1] : false;
2267
2268
		// Failsafe.
2269
		if ($pos === false || $last_pos > $pos)
2270
			$pos = strlen($message) + 1;
2271
2272
		// Can't have a one letter smiley, URL, or email! (Sorry.)
2273
		if ($last_pos < $pos - 1)
2274
		{
2275
			// Make sure the $last_pos is not negative.
2276
			$last_pos = max($last_pos, 0);
2277
2278
			// Pick a block of data to do some raw fixing on.
2279
			$data = substr($message, $last_pos, $pos - $last_pos);
2280
2281
			$placeholders = array();
2282
			$placeholders_counter = 0;
2283
2284
			// Take care of some HTML!
2285
			if (!empty($modSettings['enablePostHTML']) && strpos($data, '&lt;') !== false)
2286
			{
2287
				$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);
2288
2289
				// <br> should be empty.
2290
				$empty_tags = array('br', 'hr');
2291
				foreach ($empty_tags as $tag)
2292
					$data = str_replace(array('&lt;' . $tag . '&gt;', '&lt;' . $tag . '/&gt;', '&lt;' . $tag . ' /&gt;'), '<' . $tag . '>', $data);
2293
2294
				// b, u, i, s, pre... basic tags.
2295
				$closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote', 'strong');
2296
				foreach ($closable_tags as $tag)
2297
				{
2298
					$diff = substr_count($data, '&lt;' . $tag . '&gt;') - substr_count($data, '&lt;/' . $tag . '&gt;');
2299
					$data = strtr($data, array('&lt;' . $tag . '&gt;' => '<' . $tag . '>', '&lt;/' . $tag . '&gt;' => '</' . $tag . '>'));
2300
2301
					if ($diff > 0)
2302
						$data = substr($data, 0, -1) . str_repeat('</' . $tag . '>', $diff) . substr($data, -1);
2303
				}
2304
2305
				// Do <img ...> - with security... action= -> action-.
2306
				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);
2307
				if (!empty($matches[0]))
2308
				{
2309
					$replaces = array();
2310
					foreach ($matches[2] as $match => $imgtag)
2311
					{
2312
						$alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^&quot;|&quot;$~', '', $matches[3][$match]);
2313
2314
						// Remove action= from the URL - no funny business, now.
2315
						// @todo Testing this preg_match seems pointless
2316
						if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) != 0)
2317
							$imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag);
2318
2319
						$placeholder = '<placeholder ' . ++$placeholders_counter . '>';
2320
						$placeholders[$placeholder] = '[img' . $alt . ']' . $imgtag . '[/img]';
2321
2322
						$replaces[$matches[0][$match]] = $placeholder;
2323
					}
2324
2325
					$data = strtr($data, $replaces);
2326
				}
2327
			}
2328
2329
			if (!empty($modSettings['autoLinkUrls']))
2330
			{
2331
				// Are we inside tags that should be auto linked?
2332
				$no_autolink_area = false;
2333
				if (!empty($open_tags))
2334
				{
2335
					foreach ($open_tags as $open_tag)
2336
						if (in_array($open_tag['tag'], $no_autolink_tags))
2337
							$no_autolink_area = true;
2338
				}
2339
2340
				// Don't go backwards.
2341
				// @todo Don't think is the real solution....
2342
				$lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0;
2343
				if ($pos < $lastAutoPos)
2344
					$no_autolink_area = true;
2345
				$lastAutoPos = $pos;
2346
2347
				if (!$no_autolink_area)
2348
				{
2349
					// An &nbsp; right after a URL can break the autolinker
2350
					if (strpos($data, '&nbsp;') !== false)
2351
					{
2352
						$placeholders['<placeholder non-breaking-space>'] = '&nbsp;';
2353
						$data = strtr($data, array('&nbsp;' => '<placeholder non-breaking-space>'));
2354
					}
2355
2356
					// Parse any URLs
2357
					if (!isset($disabled['url']) && strpos($data, '[url') === false)
2358
					{
2359
						// For efficiency, first define the TLD regex in a PCRE subroutine
2360
						$url_regex = '(?(DEFINE)(?<tlds>' . $modSettings['tld_regex'] . '))';
2361
2362
						// Now build the rest of the regex
2363
						$url_regex .=
2364
						// 1. IRI scheme and domain components
2365
						'(?:' .
2366
							// 1a. IRIs with a scheme, or at least an opening "//"
2367
							'(?:' .
2368
2369
								// URI scheme (or lack thereof for schemeless URLs)
2370
								'(?:' .
2371
									// URL scheme and colon
2372
									'\b[a-z][\w\-]+:' .
2373
									// or
2374
									'|' .
2375
									// A boundary followed by two slashes for schemeless URLs
2376
									'(?<=^|\W)(?=//)' .
2377
								')' .
2378
2379
								// IRI "authority" chunk
2380
								'(?:' .
2381
									// 2 slashes for IRIs with an "authority"
2382
									'//' .
2383
									// then a domain name
2384
									'(?:' .
2385
										// Either the reserved "localhost" domain name
2386
										'localhost' .
2387
										// or
2388
										'|' .
2389
										// a run of Unicode domain name characters and a dot
2390
										'[\p{L}\p{M}\p{N}\-.:@]+\.' .
2391
										// and then a TLD
2392
										'(?:' .
2393
											// Either a TLD valid in the DNS
2394
											'(?P>tlds)' .
2395
											// or
2396
											'|' .
2397
											// the reserved "local" TLD
2398
											'local' .
2399
										')' .
2400
									')' .
2401
									// followed by a non-domain character or end of line
2402
									'(?=[^\p{L}\p{N}\-.]|$)' .
2403
2404
									// or, if no "authority" per se (e.g. "mailto:" URLs)...
2405
									'|' .
2406
2407
									// a run of IRI characters
2408
									'[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.:@]+[\p{L}\p{M}\p{N}]' .
2409
									// and then a dot and a closing IRI label
2410
									'\.[\p{L}\p{M}\p{N}\-]+' .
2411
								')' .
2412
							')' .
2413
2414
							// Or
2415
							'|' .
2416
2417
							// 1b. Naked domains (e.g. "example.com" in "Go to example.com for an example.")
2418
							'(?:' .
2419
								// Preceded by start of line or a non-domain character
2420
								'(?<=^|[^\p{L}\p{M}\p{N}\-:@])' .
2421
								// A run of Unicode domain name characters (excluding [:@])
2422
								'[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.]+[\p{L}\p{M}\p{N}]' .
2423
								// and then a dot and a valid TLD
2424
								'\.(?P>tlds)' .
2425
								// Followed by either:
2426
								'(?=' .
2427
									// end of line or a non-domain character (excluding [.:@])
2428
									'$|[^\p{L}\p{N}\-]' .
2429
									// or
2430
									'|' .
2431
									// a dot followed by end of line or a non-domain character (excluding [.:@])
2432
									'\.(?=$|[^\p{L}\p{N}\-])' .
2433
								')' .
2434
							')' .
2435
						')' .
2436
2437
						// 2. IRI path, query, and fragment components (if present)
2438
						'(?:' .
2439
2440
							// If any of these parts exist, must start with a single "/"
2441
							'/' .
2442
2443
							// And then optionally:
2444
							'(?:' .
2445
								// One or more of:
2446
								'(?:' .
2447
									// a run of non-space, non-()<>
2448
									'[^\s()<>]+' .
2449
									// or
2450
									'|' .
2451
									// balanced parentheses, up to 2 levels
2452
									'\(([^\s()<>]+|(\([^\s()<>]+\)))*\)' .
2453
								')+' .
2454
								// Ending with:
2455
								'(?:' .
2456
									// balanced parentheses, up to 2 levels
2457
									'\(([^\s()<>]+|(\([^\s()<>]+\)))*\)' .
2458
									// or
2459
									'|' .
2460
									// not a space or one of these punctuation characters
2461
									'[^\s`!()\[\]{};:\'".,<>?«»“”‘’/]' .
2462
									// or
2463
									'|' .
2464
									// a trailing slash (but not two in a row)
2465
									'(?<!/)/' .
2466
								')' .
2467
							')?' .
2468
						')?';
2469
2470
						$data = preg_replace_callback('~' . $url_regex . '~i' . ($context['utf8'] ? 'u' : ''), function($matches)
2471
						{
2472
							$url = array_shift($matches);
2473
2474
							// If this isn't a clean URL, bail out
2475
							if ($url != sanitize_iri($url))
2476
								return $url;
2477
2478
							$scheme = parse_url($url, PHP_URL_SCHEME);
2479
2480
							if ($scheme == 'mailto')
2481
							{
2482
								$email_address = str_replace('mailto:', '', $url);
2483
								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...
2484
									return '[email=' . $email_address . ']' . $url . '[/email]';
2485
								else
2486
									return $url;
2487
							}
2488
2489
							// Are we linking a schemeless URL or naked domain name (e.g. "example.com")?
2490
							if (empty($scheme))
2491
								$fullUrl = '//' . ltrim($url, ':/');
2492
							else
2493
								$fullUrl = $url;
2494
2495
							// Make sure that $fullUrl really is valid
2496
							if (validate_iri((strpos($fullUrl, '//') === 0 ? 'http:' : '') . $fullUrl) === false)
2497
								return $url;
2498
2499
							return '[url=&quot;' . str_replace(array('[', ']'), array('&#91;', '&#93;'), $fullUrl) . '&quot;]' . $url . '[/url]';
2500
						}, $data);
2501
					}
2502
2503
					// Next, emails...  Must be careful not to step on enablePostHTML logic above...
2504
					if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false && stripos($data, 'mailto:') === false)
2505
					{
2506
						$email_regex = '
2507
						# Preceded by a non-domain character or start of line
2508
						(?<=^|[^\p{L}\p{M}\p{N}\-\.])
2509
2510
						# An email address
2511
						[\p{L}\p{M}\p{N}_\-.]{1,80}
2512
						@
2513
						[\p{L}\p{M}\p{N}\-.]+
2514
						\.
2515
						' . $modSettings['tld_regex'] . '
2516
2517
						# Followed by either:
2518
						(?=
2519
							# end of line or a non-domain character (excluding the dot)
2520
							$|[^\p{L}\p{M}\p{N}\-]
2521
							| # or
2522
							# a dot followed by end of line or a non-domain character
2523
							\.(?=$|[^\p{L}\p{M}\p{N}\-])
2524
						)';
2525
2526
						$data = preg_replace('~' . $email_regex . '~xi' . ($context['utf8'] ? 'u' : ''), '[email]$0[/email]', $data);
2527
					}
2528
				}
2529
			}
2530
2531
			// Restore any placeholders
2532
			$data = strtr($data, $placeholders);
2533
2534
			$data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));
2535
2536
			// If it wasn't changed, no copying or other boring stuff has to happen!
2537
			if ($data != substr($message, $last_pos, $pos - $last_pos))
2538
			{
2539
				$message = substr($message, 0, $last_pos) . $data . substr($message, $pos);
2540
2541
				// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
2542
				$old_pos = strlen($data) + $last_pos;
2543
				$pos = strpos($message, '[', $last_pos);
2544
				$pos = $pos === false ? $old_pos : min($pos, $old_pos);
2545
			}
2546
		}
2547
2548
		// Are we there yet?  Are we there yet?
2549
		if ($pos >= strlen($message) - 1)
2550
			break;
2551
2552
		$tag_character = strtolower($message[$pos + 1]);
2553
2554
		if ($tag_character == '/' && !empty($open_tags))
2555
		{
2556
			$pos2 = strpos($message, ']', $pos + 1);
2557
			if ($pos2 == $pos + 2)
2558
				continue;
2559
2560
			$look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));
2561
2562
			// A closing tag that doesn't match any open tags? Skip it.
2563
			if (!in_array($look_for, array_map(function($code)
2564
			{
2565
				return $code['tag'];
2566
			}, $open_tags)))
2567
				continue;
2568
2569
			$to_close = array();
2570
			$block_level = null;
2571
2572
			do
2573
			{
2574
				$tag = array_pop($open_tags);
2575
				if (!$tag)
2576
					break;
2577
2578
				if (!empty($tag['block_level']))
2579
				{
2580
					// Only find out if we need to.
2581
					if ($block_level === false)
2582
					{
2583
						array_push($open_tags, $tag);
2584
						break;
2585
					}
2586
2587
					// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
2588
					if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]]))
2589
					{
2590
						foreach ($bbc_codes[$look_for[0]] as $temp)
2591
							if ($temp['tag'] == $look_for)
2592
							{
2593
								$block_level = !empty($temp['block_level']);
2594
								break;
2595
							}
2596
					}
2597
2598
					if ($block_level !== true)
2599
					{
2600
						$block_level = false;
2601
						array_push($open_tags, $tag);
2602
						break;
2603
					}
2604
				}
2605
2606
				$to_close[] = $tag;
2607
			}
2608
			while ($tag['tag'] != $look_for);
2609
2610
			// Did we just eat through everything and not find it?
2611
			if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
2612
			{
2613
				$open_tags = $to_close;
2614
				continue;
2615
			}
2616
			elseif (!empty($to_close) && $tag['tag'] != $look_for)
2617
			{
2618
				if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]]))
2619
				{
2620
					foreach ($bbc_codes[$look_for[0]] as $temp)
2621
						if ($temp['tag'] == $look_for)
2622
						{
2623
							$block_level = !empty($temp['block_level']);
2624
							break;
2625
						}
2626
				}
2627
2628
				// We're not looking for a block level tag (or maybe even a tag that exists...)
2629
				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...
2630
				{
2631
					foreach ($to_close as $tag)
2632
						array_push($open_tags, $tag);
2633
					continue;
2634
				}
2635
			}
2636
2637
			foreach ($to_close as $tag)
2638
			{
2639
				$message = substr($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1);
2640
				$pos += strlen($tag['after']) + 2;
2641
				$pos2 = $pos - 1;
2642
2643
				// See the comment at the end of the big loop - just eating whitespace ;).
2644
				$whitespace_regex = '';
2645
				if (!empty($tag['block_level']))
2646
					$whitespace_regex .= '(&nbsp;|\s)*(<br>)?';
2647
				// Trim one line of whitespace after unnested tags, but all of it after nested ones
2648
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
2649
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2650
2651
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
2652
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2653
			}
2654
2655
			if (!empty($to_close))
2656
			{
2657
				$to_close = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $to_close is dead and can be removed.
Loading history...
2658
				$pos--;
2659
			}
2660
2661
			continue;
2662
		}
2663
2664
		// No tags for this character, so just keep going (fastest possible course.)
2665
		if (!isset($bbc_codes[$tag_character]))
2666
			continue;
2667
2668
		$inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
2669
		$tag = null;
2670
		foreach ($bbc_codes[$tag_character] as $possible)
2671
		{
2672
			$pt_strlen = strlen($possible['tag']);
2673
2674
			// Not a match?
2675
			if (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag'])
2676
				continue;
2677
2678
			$next_c = isset($message[$pos + 1 + $pt_strlen]) ? $message[$pos + 1 + $pt_strlen] : '';
2679
2680
			// A tag is the last char maybe
2681
			if ($next_c == '')
2682
				break;
2683
2684
			// A test validation?
2685
			if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0)
2686
				continue;
2687
			// Do we want parameters?
2688
			elseif (!empty($possible['parameters']))
2689
			{
2690
				// Are all the parameters optional?
2691
				$param_required = false;
2692
				foreach ($possible['parameters'] as $param)
2693
				{
2694
					if (empty($param['optional']))
2695
					{
2696
						$param_required = true;
2697
						break;
2698
					}
2699
				}
2700
2701
				if ($param_required && $next_c != ' ')
2702
					continue;
2703
			}
2704
			elseif (isset($possible['type']))
2705
			{
2706
				// Do we need an equal sign?
2707
				if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=')
2708
					continue;
2709
				// Maybe we just want a /...
2710
				if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + $pt_strlen, 2) != '/]' && substr($message, $pos + 1 + $pt_strlen, 3) != ' /]')
2711
					continue;
2712
				// An immediate ]?
2713
				if ($possible['type'] == 'unparsed_content' && $next_c != ']')
2714
					continue;
2715
			}
2716
			// No type means 'parsed_content', which demands an immediate ] without parameters!
2717
			elseif ($next_c != ']')
2718
				continue;
2719
2720
			// Check allowed tree?
2721
			if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
2722
				continue;
2723
			elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
2724
				continue;
2725
			// If this is in the list of disallowed child tags, don't parse it.
2726
			elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
2727
				continue;
2728
2729
			$pos1 = $pos + 1 + $pt_strlen + 1;
2730
2731
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
2732
			if ($possible['tag'] == 'quote')
2733
			{
2734
				// Start with standard
2735
				$quote_alt = false;
2736
				foreach ($open_tags as $open_quote)
2737
				{
2738
					// Every parent quote this quote has flips the styling
2739
					if ($open_quote['tag'] == 'quote')
2740
						$quote_alt = !$quote_alt;
0 ignored issues
show
introduced by
The condition $quote_alt is always false.
Loading history...
2741
				}
2742
				// Add a class to the quote to style alternating blockquotes
2743
				$possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">'));
2744
			}
2745
2746
			// This is long, but it makes things much easier and cleaner.
2747
			if (!empty($possible['parameters']))
2748
			{
2749
				// Build a regular expression for each parameter for the current tag.
2750
				$regex_key = $smcFunc['json_encode']($possible['parameters']);
2751
				if (!isset($params_regexes[$regex_key]))
2752
				{
2753
					$params_regexes[$regex_key] = '';
2754
2755
					foreach ($possible['parameters'] as $p => $info)
2756
						$params_regexes[$regex_key] .= '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . '\s*)' . (empty($info['optional']) ? '' : '?');
2757
				}
2758
2759
				// Extract the string that potentially holds our parameters.
2760
				$blob = preg_split('~\[/?(?:' . $alltags_regex . ')~i', substr($message, $pos));
2761
				$blobs = preg_split('~\]~i', $blob[1]);
2762
2763
				$splitters = implode('=|', array_keys($possible['parameters'])) . '=';
2764
2765
				// Progressively append more blobs until we find our parameters or run out of blobs
2766
				$blob_counter = 1;
2767
				while ($blob_counter <= count($blobs))
0 ignored issues
show
Bug introduced by
It seems like $blobs can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

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

2767
				while ($blob_counter <= count(/** @scrutinizer ignore-type */ $blobs))
Loading history...
2768
				{
2769
					$given_param_string = implode(']', array_slice($blobs, 0, $blob_counter++));
0 ignored issues
show
Bug introduced by
It seems like $blobs can also be of type false; however, parameter $array of array_slice() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

2769
					$given_param_string = implode(']', array_slice(/** @scrutinizer ignore-type */ $blobs, 0, $blob_counter++));
Loading history...
2770
2771
					$given_params = preg_split('~\s(?=(' . $splitters . '))~i', $given_param_string);
2772
					sort($given_params, SORT_STRING);
0 ignored issues
show
Bug introduced by
It seems like $given_params can also be of type false; however, parameter $array of sort() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

2772
					sort(/** @scrutinizer ignore-type */ $given_params, SORT_STRING);
Loading history...
2773
2774
					$match = preg_match('~^' . $params_regexes[$regex_key] . '$~i', implode(' ', $given_params), $matches) !== 0;
2775
2776
					if ($match)
2777
						break;
2778
				}
2779
2780
				// Didn't match our parameter list, try the next possible.
2781
				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...
2782
					continue;
2783
2784
				$params = array();
2785
				for ($i = 1, $n = count($matches); $i < $n; $i += 2)
2786
				{
2787
					$key = strtok(ltrim($matches[$i]), '=');
2788
					if ($key === false)
2789
						continue;
2790
					elseif (isset($possible['parameters'][$key]['value']))
2791
						$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
2792
					elseif (isset($possible['parameters'][$key]['validate']))
2793
						$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
2794
					else
2795
						$params['{' . $key . '}'] = $matches[$i + 1];
2796
2797
					// Just to make sure: replace any $ or { so they can't interpolate wrongly.
2798
					$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
2799
				}
2800
2801
				foreach ($possible['parameters'] as $p => $info)
2802
				{
2803
					if (!isset($params['{' . $p . '}']))
2804
					{
2805
						if (!isset($info['default']))
2806
							$params['{' . $p . '}'] = '';
2807
						elseif (isset($possible['parameters'][$p]['value']))
2808
							$params['{' . $p . '}'] = strtr($possible['parameters'][$p]['value'], array('$1' => $info['default']));
2809
						elseif (isset($possible['parameters'][$p]['validate']))
2810
							$params['{' . $p . '}'] = $possible['parameters'][$p]['validate']($info['default']);
2811
						else
2812
							$params['{' . $p . '}'] = $info['default'];
2813
					}
2814
				}
2815
2816
				$tag = $possible;
2817
2818
				// Put the parameters into the string.
2819
				if (isset($tag['before']))
2820
					$tag['before'] = strtr($tag['before'], $params);
2821
				if (isset($tag['after']))
2822
					$tag['after'] = strtr($tag['after'], $params);
2823
				if (isset($tag['content']))
2824
					$tag['content'] = strtr($tag['content'], $params);
2825
2826
				$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...
2827
			}
2828
			else
2829
			{
2830
				$tag = $possible;
2831
				$params = array();
2832
			}
2833
			break;
2834
		}
2835
2836
		// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
2837
		if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
2838
		{
2839
			if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
2840
				continue;
2841
2842
			$tag = $itemcodes[$message[$pos + 1]];
2843
2844
			// First let's set up the tree: it needs to be in a list, or after an li.
2845
			if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
2846
			{
2847
				$open_tags[] = array(
2848
					'tag' => 'list',
2849
					'after' => '</ul>',
2850
					'block_level' => true,
2851
					'require_children' => array('li'),
2852
					'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2853
				);
2854
				$code = '<ul class="bbc_list">';
2855
			}
2856
			// We're in a list item already: another itemcode?  Close it first.
2857
			elseif ($inside['tag'] == 'li')
2858
			{
2859
				array_pop($open_tags);
2860
				$code = '</li>';
2861
			}
2862
			else
2863
				$code = '';
2864
2865
			// Now we open a new tag.
2866
			$open_tags[] = array(
2867
				'tag' => 'li',
2868
				'after' => '</li>',
2869
				'trim' => 'outside',
2870
				'block_level' => true,
2871
				'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2872
			);
2873
2874
			// First, open the tag...
2875
			$code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
2876
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
2877
			$pos += strlen($code) - 1 + 2;
2878
2879
			// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
2880
			$pos2 = strpos($message, '<br>', $pos);
2881
			$pos3 = strpos($message, '[/', $pos);
2882
			if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
2883
			{
2884
				preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 4), $matches);
2885
				$message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
2886
2887
				$open_tags[count($open_tags) - 2]['after'] = '</ul>';
2888
			}
2889
			// Tell the [list] that it needs to close specially.
2890
			else
2891
			{
2892
				// Move the li over, because we're not sure what we'll hit.
2893
				$open_tags[count($open_tags) - 1]['after'] = '';
2894
				$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
2895
			}
2896
2897
			continue;
2898
		}
2899
2900
		// Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
2901
		if ($tag === null && $inside !== null && !empty($inside['require_children']))
2902
		{
2903
			array_pop($open_tags);
2904
2905
			$message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
2906
			$pos += strlen($inside['after']) - 1 + 2;
2907
		}
2908
2909
		// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
2910
		if ($tag === null)
2911
			continue;
2912
2913
		// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
2914
		if (isset($inside['disallow_children']))
2915
			$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
2916
2917
		// Is this tag disabled?
2918
		if (isset($disabled[$tag['tag']]))
2919
		{
2920
			if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
2921
			{
2922
				$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
2923
				$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
2924
				$tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
2925
			}
2926
			elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
2927
			{
2928
				$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
2929
				$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
2930
			}
2931
			else
2932
				$tag['content'] = $tag['disabled_content'];
2933
		}
2934
2935
		// we use this a lot
2936
		$tag_strlen = strlen($tag['tag']);
2937
2938
		// The only special case is 'html', which doesn't need to close things.
2939
		if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
2940
		{
2941
			$n = count($open_tags) - 1;
2942
			while (empty($open_tags[$n]['block_level']) && $n >= 0)
2943
				$n--;
2944
2945
			// Close all the non block level tags so this tag isn't surrounded by them.
2946
			for ($i = count($open_tags) - 1; $i > $n; $i--)
2947
			{
2948
				$message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
2949
				$ot_strlen = strlen($open_tags[$i]['after']);
2950
				$pos += $ot_strlen + 2;
2951
				$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...
2952
2953
				// Trim or eat trailing stuff... see comment at the end of the big loop.
2954
				$whitespace_regex = '';
2955
				if (!empty($tag['block_level']))
2956
					$whitespace_regex .= '(&nbsp;|\s)*(<br>)?';
2957
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
2958
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2959
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
2960
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2961
2962
				array_pop($open_tags);
2963
			}
2964
		}
2965
2966
		// Can't read past the end of the message
2967
		$pos1 = min(strlen($message), $pos1);
2968
2969
		// No type means 'parsed_content'.
2970
		if (!isset($tag['type']))
2971
		{
2972
			$open_tags[] = $tag;
2973
			$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
2974
			$pos += strlen($tag['before']) - 1 + 2;
2975
		}
2976
		// Don't parse the content, just skip it.
2977
		elseif ($tag['type'] == 'unparsed_content')
2978
		{
2979
			$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
2980
			if ($pos2 === false)
2981
				continue;
2982
2983
			$data = substr($message, $pos1, $pos2 - $pos1);
2984
2985
			if (!empty($tag['block_level']) && substr($data, 0, 4) == '<br>')
2986
				$data = substr($data, 4);
2987
2988
			if (isset($tag['validate']))
2989
				$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...
2990
2991
			$code = strtr($tag['content'], array('$1' => $data));
2992
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
2993
2994
			$pos += strlen($code) - 1 + 2;
2995
			$last_pos = $pos + 1;
2996
		}
2997
		// Don't parse the content, just skip it.
2998
		elseif ($tag['type'] == 'unparsed_equals_content')
2999
		{
3000
			// The value may be quoted for some tags - check.
3001
			if (isset($tag['quoted']))
3002
			{
3003
				$quoted = substr($message, $pos1, 6) == '&quot;';
3004
				if ($tag['quoted'] != 'optional' && !$quoted)
3005
					continue;
3006
3007
				if ($quoted)
3008
					$pos1 += 6;
3009
			}
3010
			else
3011
				$quoted = false;
3012
3013
			$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...
3014
			if ($pos2 === false)
3015
				continue;
3016
3017
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3018
			if ($pos3 === false)
3019
				continue;
3020
3021
			$data = array(
3022
				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...
3023
				substr($message, $pos1, $pos2 - $pos1)
3024
			);
3025
3026
			if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '<br>')
3027
				$data[0] = substr($data[0], 4);
3028
3029
			// Validation for my parking, please!
3030
			if (isset($tag['validate']))
3031
				$tag['validate']($tag, $data, $disabled, $params);
3032
3033
			$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
3034
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3035
			$pos += strlen($code) - 1 + 2;
3036
		}
3037
		// A closed tag, with no content or value.
3038
		elseif ($tag['type'] == 'closed')
3039
		{
3040
			$pos2 = strpos($message, ']', $pos);
3041
			$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
3042
			$pos += strlen($tag['content']) - 1 + 2;
3043
		}
3044
		// This one is sorta ugly... :/.  Unfortunately, it's needed for flash.
3045
		elseif ($tag['type'] == 'unparsed_commas_content')
3046
		{
3047
			$pos2 = strpos($message, ']', $pos1);
3048
			if ($pos2 === false)
3049
				continue;
3050
3051
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3052
			if ($pos3 === false)
3053
				continue;
3054
3055
			// We want $1 to be the content, and the rest to be csv.
3056
			$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
3057
			$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
3058
3059
			if (isset($tag['validate']))
3060
				$tag['validate']($tag, $data, $disabled, $params);
3061
3062
			$code = $tag['content'];
3063
			foreach ($data as $k => $d)
3064
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3065
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3066
			$pos += strlen($code) - 1 + 2;
3067
		}
3068
		// This has parsed content, and a csv value which is unparsed.
3069
		elseif ($tag['type'] == 'unparsed_commas')
3070
		{
3071
			$pos2 = strpos($message, ']', $pos1);
3072
			if ($pos2 === false)
3073
				continue;
3074
3075
			$data = explode(',', substr($message, $pos1, $pos2 - $pos1));
3076
3077
			if (isset($tag['validate']))
3078
				$tag['validate']($tag, $data, $disabled, $params);
3079
3080
			// Fix after, for disabled code mainly.
3081
			foreach ($data as $k => $d)
3082
				$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
3083
3084
			$open_tags[] = $tag;
3085
3086
			// Replace them out, $1, $2, $3, $4, etc.
3087
			$code = $tag['before'];
3088
			foreach ($data as $k => $d)
3089
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3090
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1);
3091
			$pos += strlen($code) - 1 + 2;
3092
		}
3093
		// A tag set to a value, parsed or not.
3094
		elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
3095
		{
3096
			// The value may be quoted for some tags - check.
3097
			if (isset($tag['quoted']))
3098
			{
3099
				$quoted = substr($message, $pos1, 6) == '&quot;';
3100
				if ($tag['quoted'] != 'optional' && !$quoted)
3101
					continue;
3102
3103
				if ($quoted)
3104
					$pos1 += 6;
3105
			}
3106
			else
3107
				$quoted = false;
3108
3109
			$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...
3110
			if ($pos2 === false)
3111
				continue;
3112
3113
			$data = substr($message, $pos1, $pos2 - $pos1);
3114
3115
			// Validation for my parking, please!
3116
			if (isset($tag['validate']))
3117
				$tag['validate']($tag, $data, $disabled, $params);
3118
3119
			// For parsed content, we must recurse to avoid security problems.
3120
			if ($tag['type'] != 'unparsed_equals')
3121
				$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
3122
3123
			$tag['after'] = strtr($tag['after'], array('$1' => $data));
3124
3125
			$open_tags[] = $tag;
3126
3127
			$code = strtr($tag['before'], array('$1' => $data));
3128
			$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...
3129
			$pos += strlen($code) - 1 + 2;
3130
		}
3131
3132
		// If this is block level, eat any breaks after it.
3133
		if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '<br>')
3134
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 5);
3135
3136
		// Are we trimming outside this tag?
3137
		if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
3138
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
3139
	}
3140
3141
	// Close any remaining tags.
3142
	while ($tag = array_pop($open_tags))
3143
		$message .= "\n" . $tag['after'] . "\n";
3144
3145
	// Parse the smileys within the parts where it can be done safely.
3146
	if ($smileys === true)
3147
	{
3148
		$message_parts = explode("\n", $message);
3149
		for ($i = 0, $n = count($message_parts); $i < $n; $i += 2)
3150
			parsesmileys($message_parts[$i]);
3151
3152
		$message = implode('', $message_parts);
3153
	}
3154
3155
	// No smileys, just get rid of the markers.
3156
	else
3157
		$message = strtr($message, array("\n" => ''));
3158
3159
	if ($message !== '' && $message[0] === ' ')
3160
		$message = '&nbsp;' . substr($message, 1);
3161
3162
	// Cleanup whitespace.
3163
	$message = strtr($message, array('  ' => ' &nbsp;', "\r" => '', "\n" => '<br>', '<br> ' => '<br>&nbsp;', '&#13;' => "\n"));
3164
3165
	// Allow mods access to what parse_bbc created
3166
	call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
3167
3168
	// Cache the output if it took some time...
3169
	if (isset($cache_key, $cache_t) && microtime(true) - $cache_t > 0.05)
3170
		cache_put_data($cache_key, $message, 240);
3171
3172
	// If this was a force parse revert if needed.
3173
	if (!empty($parse_tags))
3174
	{
3175
		$alltags_regex = empty($real_alltags_regex) ? '' : $real_alltags_regex;
3176
		unset($real_alltags_regex);
3177
	}
3178
	elseif (!empty($bbc_codes))
3179
		$bbc_lang_locales[$txt['lang_locale']] = $bbc_codes;
3180
3181
	return $message;
3182
}
3183
3184
/**
3185
 * Parse smileys in the passed message.
3186
 *
3187
 * The smiley parsing function which makes pretty faces appear :).
3188
 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
3189
 * These are specifically not parsed in code tags [url=mailto:[email protected]]
3190
 * Caches the smileys from the database or array in memory.
3191
 * Doesn't return anything, but rather modifies message directly.
3192
 *
3193
 * @param string &$message The message to parse smileys in
3194
 */
3195
function parsesmileys(&$message)
3196
{
3197
	global $modSettings, $txt, $user_info, $context, $smcFunc;
3198
	static $smileyPregSearch = null, $smileyPregReplacements = array();
3199
3200
	// No smiley set at all?!
3201
	if ($user_info['smiley_set'] == 'none' || trim($message) == '')
3202
		return;
3203
3204
	// Maybe a mod wants to implement an alternative method (e.g. emojis instead of images)
3205
	call_integration_hook('integrate_smileys', array(&$smileyPregSearch, &$smileyPregReplacements));
3206
3207
	// If smileyPregSearch hasn't been set, do it now.
3208
	if (empty($smileyPregSearch))
3209
	{
3210
		// Cache for longer when customized smiley codes aren't enabled
3211
		$cache_time = empty($modSettings['smiley_enable']) ? 7200 : 480;
3212
3213
		// Load the smileys in reverse order by length so they don't get parsed incorrectly.
3214
		if (($temp = cache_get_data('parsing_smileys_' . $user_info['smiley_set'], $cache_time)) == null)
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $temp = cache_get_data('...ley_set'], $cache_time) of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
3215
		{
3216
			$result = $smcFunc['db_query']('', '
3217
				SELECT s.code, f.filename, s.description
3218
				FROM {db_prefix}smileys AS s
3219
					JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
3220
				WHERE f.smiley_set = {string:smiley_set}' . (empty($modSettings['smiley_enable']) ? '
3221
					AND s.code IN ({array_string:default_codes})' : '') . '
3222
				ORDER BY LENGTH(s.code) DESC',
3223
				array(
3224
					'default_codes' => array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'),
3225
					'smiley_set' => $user_info['smiley_set'],
3226
				)
3227
			);
3228
			$smileysfrom = array();
3229
			$smileysto = array();
3230
			$smileysdescs = array();
3231
			while ($row = $smcFunc['db_fetch_assoc']($result))
3232
			{
3233
				$smileysfrom[] = $row['code'];
3234
				$smileysto[] = $smcFunc['htmlspecialchars']($row['filename']);
3235
				$smileysdescs[] = !empty($txt['icon_' . strtolower($row['description'])]) ? $txt['icon_' . strtolower($row['description'])] : $row['description'];
3236
			}
3237
			$smcFunc['db_free_result']($result);
3238
3239
			cache_put_data('parsing_smileys_' . $user_info['smiley_set'], array($smileysfrom, $smileysto, $smileysdescs), $cache_time);
3240
		}
3241
		else
3242
			list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
3243
3244
		// The non-breaking-space is a complex thing...
3245
		$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
3246
3247
		// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:[email protected]] doesn't parse the :D smiley)
3248
		$smileyPregReplacements = array();
3249
		$searchParts = array();
3250
		$smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
3251
3252
		for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
3253
		{
3254
			$specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES);
3255
			$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">';
3256
3257
			$smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
3258
3259
			$searchParts[] = $smileysfrom[$i];
3260
			if ($smileysfrom[$i] != $specialChars)
3261
			{
3262
				$smileyPregReplacements[$specialChars] = $smileyCode;
3263
				$searchParts[] = $specialChars;
3264
3265
				// Some 2.0 hex htmlchars are in there as 3 digits; allow for finding leading 0 or not
3266
				$specialChars2 = preg_replace('/&#(\d{2});/', '&#0$1;', $specialChars);
3267
				if ($specialChars2 != $specialChars)
3268
				{
3269
					$smileyPregReplacements[$specialChars2] = $smileyCode;
3270
					$searchParts[] = $specialChars2;
3271
				}
3272
			}
3273
		}
3274
3275
		$smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?<![a-zA-Z0-9])\(|^)(' . build_regex($searchParts, '~') . ')(?=[^[:alpha:]0-9]|$)~' . ($context['utf8'] ? 'u' : '');
3276
	}
3277
3278
	// Replace away!
3279
	$message = preg_replace_callback($smileyPregSearch, function($matches) use ($smileyPregReplacements)
3280
		{
3281
			return $smileyPregReplacements[$matches[1]];
3282
		}, $message);
3283
}
3284
3285
/**
3286
 * Highlight any code.
3287
 *
3288
 * Uses PHP's highlight_string() to highlight PHP syntax
3289
 * does special handling to keep the tabs in the code available.
3290
 * used to parse PHP code from inside [code] and [php] tags.
3291
 *
3292
 * @param string $code The code
3293
 * @return string The code with highlighted HTML.
3294
 */
3295
function highlight_php_code($code)
3296
{
3297
	// Remove special characters.
3298
	$code = un_htmlspecialchars(strtr($code, array('<br />' => "\n", '<br>' => "\n", "\t" => 'SMF_TAB();', '&#91;' => '[')));
3299
3300
	$oldlevel = error_reporting(0);
3301
3302
	$buffer = str_replace(array("\n", "\r"), '', @highlight_string($code, true));
3303
3304
	error_reporting($oldlevel);
3305
3306
	// Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
3307
	$buffer = preg_replace('~SMF_TAB(?:</(?:font|span)><(?:font color|span style)="[^"]*?">)?\\(\\);~', '<pre style="display: inline;">' . "\t" . '</pre>', $buffer);
3308
3309
	return strtr($buffer, array('\'' => '&#039;', '<code>' => '', '</code>' => ''));
3310
}
3311
3312
/**
3313
 * Gets the appropriate URL to use for images (or whatever) when using SSL
3314
 *
3315
 * The returned URL may or may not be a proxied URL, depending on the situation.
3316
 * Mods can implement alternative proxies using the 'integrate_proxy' hook.
3317
 *
3318
 * @param string $url The original URL of the requested resource
3319
 * @return string The URL to use
3320
 */
3321
function get_proxied_url($url)
3322
{
3323
	global $boardurl, $image_proxy_enabled, $image_proxy_secret;
3324
3325
	// Only use the proxy if enabled and necessary
3326
	if (empty($image_proxy_enabled) || parse_url($url, PHP_URL_SCHEME) === 'https')
3327
		return $url;
3328
3329
	// We don't need to proxy our own resources
3330
	if (strpos(strtr($url, array('http://' => 'https://')), strtr($boardurl, array('http://' => 'https://'))) === 0)
3331
		return strtr($url, array('http://' => 'https://'));
3332
3333
	// By default, use SMF's own image proxy script
3334
	$proxied_url = strtr($boardurl, array('http://' => 'https://')) . '/proxy.php?request=' . urlencode($url) . '&hash=' . md5($url . $image_proxy_secret);
3335
3336
	// Allow mods to easily implement an alternative proxy
3337
	// MOD AUTHORS: To add settings UI for your proxy, use the integrate_general_settings hook.
3338
	call_integration_hook('integrate_proxy', array($url, &$proxied_url));
3339
3340
	return $proxied_url;
3341
}
3342
3343
/**
3344
 * Make sure the browser doesn't come back and repost the form data.
3345
 * Should be used whenever anything is posted.
3346
 *
3347
 * @param string $setLocation The URL to redirect them to
3348
 * @param bool $refresh Whether to use a meta refresh instead
3349
 * @param bool $permanent Whether to send a 301 Moved Permanently instead of a 302 Moved Temporarily
3350
 */
3351
function redirectexit($setLocation = '', $refresh = false, $permanent = false)
3352
{
3353
	global $scripturl, $context, $modSettings, $db_show_debug, $db_cache;
3354
3355
	// In case we have mail to send, better do that - as obExit doesn't always quite make it...
3356
	if (!empty($context['flush_mail']))
3357
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
3358
		AddMailQueue(true);
3359
3360
	$add = preg_match('~^(ftp|http)[s]?://~', $setLocation) == 0 && substr($setLocation, 0, 6) != 'about:';
3361
3362
	if ($add)
3363
		$setLocation = $scripturl . ($setLocation != '' ? '?' . $setLocation : '');
3364
3365
	// Put the session ID in.
3366
	if (defined('SID') && SID != '')
3367
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', $scripturl . '?' . SID . ';', $setLocation);
3368
	// Keep that debug in their for template debugging!
3369
	elseif (isset($_GET['debug']))
3370
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);
3371
3372
	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'])))
3373
	{
3374
		if (defined('SID') && SID != '')
3375
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '~') . '\?(?:' . SID . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~',
3376
				function($m) use ($scripturl)
3377
				{
3378
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . SID . (isset($m[2]) ? "$m[2]" : "");
3379
				}, $setLocation);
3380
		else
3381
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '~') . '\?((?:board|topic)=[^#"]+?)(#[^"]*?)?$~',
3382
				function($m) use ($scripturl)
3383
				{
3384
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html' . (isset($m[2]) ? "$m[2]" : "");
3385
				}, $setLocation);
3386
	}
3387
3388
	// Maybe integrations want to change where we are heading?
3389
	call_integration_hook('integrate_redirect', array(&$setLocation, &$refresh, &$permanent));
3390
3391
	// Set the header.
3392
	header('location: ' . str_replace(' ', '%20', $setLocation), true, $permanent ? 301 : 302);
3393
3394
	// Debugging.
3395
	if (isset($db_show_debug) && $db_show_debug === true)
3396
		$_SESSION['debug_redirect'] = $db_cache;
3397
3398
	obExit(false);
3399
}
3400
3401
/**
3402
 * Ends execution.  Takes care of template loading and remembering the previous URL.
3403
 *
3404
 * @param bool $header Whether to do the header
3405
 * @param bool $do_footer Whether to do the footer
3406
 * @param bool $from_index Whether we're coming from the board index
3407
 * @param bool $from_fatal_error Whether we're coming from a fatal error
3408
 */
3409
function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false)
3410
{
3411
	global $context, $settings, $modSettings, $txt, $smcFunc, $should_log;
3412
	static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
3413
3414
	// Attempt to prevent a recursive loop.
3415
	++$level;
3416
	if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
3417
		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...
3418
	if ($from_fatal_error)
3419
		$has_fatal_error = true;
3420
3421
	// Clear out the stat cache.
3422
	trackStats();
3423
3424
	// If we have mail to send, send it.
3425
	if (!empty($context['flush_mail']))
3426
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
3427
		AddMailQueue(true);
3428
3429
	$do_header = $header === null ? !$header_done : $header;
3430
	if ($do_footer === null)
3431
		$do_footer = $do_header;
3432
3433
	// Has the template/header been done yet?
3434
	if ($do_header)
3435
	{
3436
		// Was the page title set last minute? Also update the HTML safe one.
3437
		if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
3438
			$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3439
3440
		// Start up the session URL fixer.
3441
		ob_start('ob_sessrewrite');
3442
3443
		if (!empty($settings['output_buffers']) && is_string($settings['output_buffers']))
3444
			$buffers = explode(',', $settings['output_buffers']);
3445
		elseif (!empty($settings['output_buffers']))
3446
			$buffers = $settings['output_buffers'];
3447
		else
3448
			$buffers = array();
3449
3450
		if (isset($modSettings['integrate_buffer']))
3451
			$buffers = array_merge(explode(',', $modSettings['integrate_buffer']), $buffers);
3452
3453
		if (!empty($buffers))
3454
			foreach ($buffers as $function)
3455
			{
3456
				$call = call_helper($function, true);
3457
3458
				// Is it valid?
3459
				if (!empty($call))
3460
					ob_start($call);
0 ignored issues
show
Bug introduced by
It seems like $call can also be of type boolean; however, parameter $output_callback of ob_start() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

5497
			$fetch_data->get_url_data($url, /** @scrutinizer ignore-type */ $post_data);
Loading history...
5498
5499
			// no errors and a 200 result, then we have a good dataset, well we at least have data. ;)
5500
			if ($fetch_data->result('code') == 200 && !$fetch_data->result('error'))
5501
				$data = $fetch_data->result('body');
5502
			else
5503
				return false;
5504
		}
5505
5506
		// Neither fsockopen nor curl are available. Well, phooey.
5507
		else
5508
			return false;
5509
	}
5510
	else
5511
	{
5512
		// Umm, this shouldn't happen?
5513
		trigger_error('fetch_web_data(): Bad URL', E_USER_NOTICE);
5514
		$data = false;
5515
	}
5516
5517
	return $data;
5518
}
5519
5520
/**
5521
 * Prepares an array of "likes" info for the topic specified by $topic
5522
 *
5523
 * @param integer $topic The topic ID to fetch the info from.
5524
 * @return array An array of IDs of messages in the specified topic that the current user likes
5525
 */
5526
function prepareLikesContext($topic)
5527
{
5528
	global $user_info, $smcFunc;
5529
5530
	// Make sure we have something to work with.
5531
	if (empty($topic))
5532
		return array();
5533
5534
	// We already know the number of likes per message, we just want to know whether the current user liked it or not.
5535
	$user = $user_info['id'];
5536
	$cache_key = 'likes_topic_' . $topic . '_' . $user;
5537
	$ttl = 180;
5538
5539
	if (($temp = cache_get_data($cache_key, $ttl)) === null)
5540
	{
5541
		$temp = array();
5542
		$request = $smcFunc['db_query']('', '
5543
			SELECT content_id
5544
			FROM {db_prefix}user_likes AS l
5545
				INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg)
5546
			WHERE l.id_member = {int:current_user}
5547
				AND l.content_type = {literal:msg}
5548
				AND m.id_topic = {int:topic}',
5549
			array(
5550
				'current_user' => $user,
5551
				'topic' => $topic,
5552
			)
5553
		);
5554
		while ($row = $smcFunc['db_fetch_assoc']($request))
5555
			$temp[] = (int) $row['content_id'];
5556
5557
		cache_put_data($cache_key, $temp, $ttl);
5558
	}
5559
5560
	return $temp;
5561
}
5562
5563
/**
5564
 * Microsoft uses their own character set Code Page 1252 (CP1252), which is a
5565
 * superset of ISO 8859-1, defining several characters between DEC 128 and 159
5566
 * that are not normally displayable.  This converts the popular ones that
5567
 * appear from a cut and paste from windows.
5568
 *
5569
 * @param string $string The string
5570
 * @return string The sanitized string
5571
 */
5572
function sanitizeMSCutPaste($string)
5573
{
5574
	global $context;
5575
5576
	if (empty($string))
5577
		return $string;
5578
5579
	// UTF-8 occurences of MS special characters
5580
	$findchars_utf8 = array(
5581
		"\xe2\x80\x9a",	// single low-9 quotation mark
5582
		"\xe2\x80\x9e",	// double low-9 quotation mark
5583
		"\xe2\x80\xa6",	// horizontal ellipsis
5584
		"\xe2\x80\x98",	// left single curly quote
5585
		"\xe2\x80\x99",	// right single curly quote
5586
		"\xe2\x80\x9c",	// left double curly quote
5587
		"\xe2\x80\x9d",	// right double curly quote
5588
		"\xe2\x80\x93",	// en dash
5589
		"\xe2\x80\x94",	// em dash
5590
	);
5591
5592
	// windows 1252 / iso equivalents
5593
	$findchars_iso = array(
5594
		chr(130),
5595
		chr(132),
5596
		chr(133),
5597
		chr(145),
5598
		chr(146),
5599
		chr(147),
5600
		chr(148),
5601
		chr(150),
5602
		chr(151),
5603
	);
5604
5605
	// safe replacements
5606
	$replacechars = array(
5607
		',',	// &sbquo;
5608
		',,',	// &bdquo;
5609
		'...',	// &hellip;
5610
		"'",	// &lsquo;
5611
		"'",	// &rsquo;
5612
		'"',	// &ldquo;
5613
		'"',	// &rdquo;
5614
		'-',	// &ndash;
5615
		'--',	// &mdash;
5616
	);
5617
5618
	if ($context['utf8'])
5619
		$string = str_replace($findchars_utf8, $replacechars, $string);
5620
	else
5621
		$string = str_replace($findchars_iso, $replacechars, $string);
5622
5623
	return $string;
5624
}
5625
5626
/**
5627
 * Decode numeric html entities to their ascii or UTF8 equivalent character.
5628
 *
5629
 * Callback function for preg_replace_callback in subs-members
5630
 * Uses capture group 2 in the supplied array
5631
 * Does basic scan to ensure characters are inside a valid range
5632
 *
5633
 * @param array $matches An array of matches (relevant info should be the 3rd item)
5634
 * @return string A fixed string
5635
 */
5636
function replaceEntities__callback($matches)
5637
{
5638
	global $context;
5639
5640
	if (!isset($matches[2]))
5641
		return '';
5642
5643
	$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

5643
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5644
5645
	// remove left to right / right to left overrides
5646
	if ($num === 0x202D || $num === 0x202E)
5647
		return '';
5648
5649
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
5650
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
5651
		return '&#' . $num . ';';
5652
5653
	if (empty($context['utf8']))
5654
	{
5655
		// no control characters
5656
		if ($num < 0x20)
5657
			return '';
5658
		// text is text
5659
		elseif ($num < 0x80)
5660
			return chr($num);
0 ignored issues
show
Bug introduced by
It seems like $num can also be of type double; however, parameter $ascii of chr() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

5660
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5661
		// all others get html-ised
5662
		else
5663
			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

5663
			return '&#' . /** @scrutinizer ignore-type */ $matches[2] . ';';
Loading history...
5664
	}
5665
	else
5666
	{
5667
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
5668
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
5669
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
5670
			return '';
5671
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
5672
		elseif ($num < 0x80)
5673
			return chr($num);
5674
		// <0x800 (2048)
5675
		elseif ($num < 0x800)
5676
			return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5677
		// < 0x10000 (65536)
5678
		elseif ($num < 0x10000)
5679
			return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5680
		// <= 0x10FFFF (1114111)
5681
		else
5682
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5683
	}
5684
}
5685
5686
/**
5687
 * Converts html entities to utf8 equivalents
5688
 *
5689
 * Callback function for preg_replace_callback
5690
 * Uses capture group 1 in the supplied array
5691
 * Does basic checks to keep characters inside a viewable range.
5692
 *
5693
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
5694
 * @return string The fixed string
5695
 */
5696
function fixchar__callback($matches)
5697
{
5698
	if (!isset($matches[1]))
5699
		return '';
5700
5701
	$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

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

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

5709
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5710
	// <0x800 (2048)
5711
	elseif ($num < 0x800)
5712
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5713
	// < 0x10000 (65536)
5714
	elseif ($num < 0x10000)
5715
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5716
	// <= 0x10FFFF (1114111)
5717
	else
5718
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5719
}
5720
5721
/**
5722
 * Strips out invalid html entities, replaces others with html style &#123; codes
5723
 *
5724
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
5725
 * strpos, strlen, substr etc
5726
 *
5727
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
5728
 * @return string The fixed string
5729
 */
5730
function entity_fix__callback($matches)
5731
{
5732
	if (!isset($matches[2]))
5733
		return '';
5734
5735
	$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

5735
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5736
5737
	// we don't allow control characters, characters out of range, byte markers, etc
5738
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
5739
		return '';
5740
	else
5741
		return '&#' . $num . ';';
5742
}
5743
5744
/**
5745
 * Return a Gravatar URL based on
5746
 * - the supplied email address,
5747
 * - the global maximum rating,
5748
 * - the global default fallback,
5749
 * - maximum sizes as set in the admin panel.
5750
 *
5751
 * It is SSL aware, and caches most of the parameters.
5752
 *
5753
 * @param string $email_address The user's email address
5754
 * @return string The gravatar URL
5755
 */
5756
function get_gravatar_url($email_address)
5757
{
5758
	global $modSettings, $smcFunc;
5759
	static $url_params = null;
5760
5761
	if ($url_params === null)
5762
	{
5763
		$ratings = array('G', 'PG', 'R', 'X');
5764
		$defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank');
5765
		$url_params = array();
5766
		if (!empty($modSettings['gravatarMaxRating']) && in_array($modSettings['gravatarMaxRating'], $ratings))
5767
			$url_params[] = 'rating=' . $modSettings['gravatarMaxRating'];
5768
		if (!empty($modSettings['gravatarDefault']) && in_array($modSettings['gravatarDefault'], $defaults))
5769
			$url_params[] = 'default=' . $modSettings['gravatarDefault'];
5770
		if (!empty($modSettings['avatar_max_width_external']))
5771
			$size_string = (int) $modSettings['avatar_max_width_external'];
5772
		if (!empty($modSettings['avatar_max_height_external']) && !empty($size_string))
5773
			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...
5774
				$size_string = $modSettings['avatar_max_height_external'];
5775
5776
		if (!empty($size_string))
5777
			$url_params[] = 's=' . $size_string;
5778
	}
5779
	$http_method = !empty($modSettings['force_ssl']) ? 'https://secure' : 'http://www';
5780
5781
	return $http_method . '.gravatar.com/avatar/' . md5($smcFunc['strtolower']($email_address)) . '?' . implode('&', $url_params);
5782
}
5783
5784
/**
5785
 * Get a list of timezones.
5786
 *
5787
 * @param string $when An optional date or time for which to calculate the timezone offset values. May be a Unix timestamp or any string that strtotime() can understand. Defaults to 'now'.
5788
 * @return array An array of timezone info.
5789
 */
5790
function smf_list_timezones($when = 'now')
5791
{
5792
	global $smcFunc, $modSettings, $tztxt, $txt;
5793
	static $timezones = null, $lastwhen = null;
5794
5795
	// No point doing this over if we already did it once
5796
	if (!empty($timezones) && $when == $lastwhen)
5797
		return $timezones;
5798
	else
5799
		$lastwhen = $when;
5800
5801
	// Parseable datetime string?
5802
	if (is_int($timestamp = strtotime($when)))
0 ignored issues
show
introduced by
The condition is_int($timestamp = strtotime($when)) is always true.
Loading history...
5803
		$when = $timestamp;
5804
5805
	// A Unix timestamp?
5806
	elseif (is_numeric($when))
5807
		$when = intval($when);
5808
5809
	// Invalid value? Just get current Unix timestamp.
5810
	else
5811
		$when = time();
5812
5813
	// We'll need these too
5814
	$date_when = date_create('@' . $when);
5815
	$later = (int) date_format(date_add($date_when, date_interval_create_from_date_string('1 year')), 'U');
0 ignored issues
show
Bug introduced by
It seems like $date_when can also be of type false; however, parameter $object of date_add() does only seem to accept DateTime, maybe add an additional type check? ( Ignorable by Annotation )

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

5815
	$later = (int) date_format(date_add(/** @scrutinizer ignore-type */ $date_when, date_interval_create_from_date_string('1 year')), 'U');
Loading history...
5816
5817
	// Load up any custom time zone descriptions we might have
5818
	loadLanguage('Timezones');
5819
5820
	// Should we put time zones from certain countries at the top of the list?
5821
	$priority_countries = !empty($modSettings['timezone_priority_countries']) ? explode(',', $modSettings['timezone_priority_countries']) : array();
5822
	$priority_tzids = array();
5823
	foreach ($priority_countries as $country)
5824
	{
5825
		$country_tzids = @timezone_identifiers_list(DateTimeZone::PER_COUNTRY, strtoupper(trim($country)));
5826
		if (!empty($country_tzids))
5827
			$priority_tzids = array_merge($priority_tzids, $country_tzids);
5828
	}
5829
5830
	// Antarctic research stations should be listed last, unless you're running a penguin forum
5831
	$low_priority_tzids = !in_array('AQ', $priority_countries) ? timezone_identifiers_list(DateTimeZone::ANTARCTICA) : array();
5832
5833
	// Process the preferred timezones first, then the normal ones, then the low priority ones.
5834
	$tzids = array_merge(array_keys($tztxt), array_diff(timezone_identifiers_list(), array_keys($tztxt), $low_priority_tzids), $low_priority_tzids);
0 ignored issues
show
Bug introduced by
It seems like $low_priority_tzids can also be of type false; however, parameter $_ of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

5834
	$tzids = array_merge(array_keys($tztxt), array_diff(timezone_identifiers_list(), array_keys($tztxt), $low_priority_tzids), /** @scrutinizer ignore-type */ $low_priority_tzids);
Loading history...
Bug introduced by
It seems like $low_priority_tzids can also be of type false; however, parameter $_ of array_diff() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

5834
	$tzids = array_merge(array_keys($tztxt), array_diff(timezone_identifiers_list(), array_keys($tztxt), /** @scrutinizer ignore-type */ $low_priority_tzids), $low_priority_tzids);
Loading history...
Bug introduced by
It seems like timezone_identifiers_list() can also be of type false; however, parameter $array1 of array_diff() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

5834
	$tzids = array_merge(array_keys($tztxt), array_diff(/** @scrutinizer ignore-type */ timezone_identifiers_list(), array_keys($tztxt), $low_priority_tzids), $low_priority_tzids);
Loading history...
5835
5836
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
5837
	foreach ($tzids as $tzid)
5838
	{
5839
		// We don't want UTC right now
5840
		if ($tzid == 'UTC')
5841
			continue;
5842
5843
		$tz = timezone_open($tzid);
5844
5845
		// First, get the set of transition rules for this tzid
5846
		$tzinfo = timezone_transitions_get($tz, $when, $later);
0 ignored issues
show
Bug introduced by
It seems like $tz can also be of type false; however, parameter $object of timezone_transitions_get() does only seem to accept DateTimeZone, maybe add an additional type check? ( Ignorable by Annotation )

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

5846
		$tzinfo = timezone_transitions_get(/** @scrutinizer ignore-type */ $tz, $when, $later);
Loading history...
5847
5848
		// Use the entire set of transition rules as the array *key* so we can avoid duplicates
5849
		$tzkey = serialize($tzinfo);
5850
5851
		// Next, get the geographic info for this tzid
5852
		$tzgeo = timezone_location_get($tz);
0 ignored issues
show
Bug introduced by
It seems like $tz can also be of type false; however, parameter $object of timezone_location_get() does only seem to accept DateTimeZone, maybe add an additional type check? ( Ignorable by Annotation )

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

5852
		$tzgeo = timezone_location_get(/** @scrutinizer ignore-type */ $tz);
Loading history...
5853
5854
		// Don't overwrite our preferred tzids
5855
		if (empty($zones[$tzkey]['tzid']))
5856
		{
5857
			$zones[$tzkey]['tzid'] = $tzid;
5858
			$zones[$tzkey]['abbr'] = $tzinfo[0]['abbr'];
5859
		}
5860
5861
		// A time zone from a prioritized country?
5862
		if (in_array($tzid, $priority_tzids))
5863
			$priority_zones[$tzkey] = true;
5864
5865
		// Keep track of the location and offset for this tzid
5866
		if (!empty($txt[$tzid]))
5867
			$zones[$tzkey]['locations'][] = $txt[$tzid];
5868
		else
5869
		{
5870
			$tzid_parts = explode('/', $tzid);
5871
			$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
5872
		}
5873
		$offsets[$tzkey] = $tzinfo[0]['offset'];
5874
		$longitudes[$tzkey] = empty($longitudes[$tzkey]) ? $tzgeo['longitude'] : $longitudes[$tzkey];
5875
	}
5876
5877
	// Sort by offset then longitude
5878
	array_multisort($offsets, SORT_ASC, SORT_NUMERIC, $longitudes, SORT_ASC, SORT_NUMERIC, $zones);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $longitudes does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $zones does not seem to be defined for all execution paths leading up to this point.
Loading history...
5879
5880
	// Build the final array of formatted values
5881
	$priority_timezones = array();
5882
	$timezones = array();
5883
	foreach ($zones as $tzkey => $tzvalue)
5884
	{
5885
		date_timezone_set($date_when, timezone_open($tzvalue['tzid']));
0 ignored issues
show
Bug introduced by
It seems like timezone_open($tzvalue['tzid']) can also be of type false; however, parameter $timezone of date_timezone_set() does only seem to accept DateTimeZone, maybe add an additional type check? ( Ignorable by Annotation )

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

5885
		date_timezone_set($date_when, /** @scrutinizer ignore-type */ timezone_open($tzvalue['tzid']));
Loading history...
Bug introduced by
It seems like $date_when can also be of type false; however, parameter $object of date_timezone_set() does only seem to accept DateTime, maybe add an additional type check? ( Ignorable by Annotation )

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

5885
		date_timezone_set(/** @scrutinizer ignore-type */ $date_when, timezone_open($tzvalue['tzid']));
Loading history...
5886
5887
		// Use the custom description, if there is one
5888
		if (!empty($tztxt[$tzvalue['tzid']]))
5889
			$desc = $tztxt[$tzvalue['tzid']];
5890
		// Otherwise, use the list of locations (max 5, so things don't get silly)
5891
		else
5892
			$desc = implode(', ', array_slice(array_unique($tzvalue['locations']), 0, 5)) . (count($tzvalue['locations']) > 5 ? ', ' . $txt['etc'] : '');
5893
5894
		// Show the UTC offset and the abbreviation, if it's something like 'MST' and not '-06'
5895
		$desc = '[UTC' . date_format($date_when, 'P') . '] - ' . (!strspn($tzvalue['abbr'], '+-') ? $tzvalue['abbr'] . ' - ' : '') . $desc;
5896
5897
		if (isset($priority_zones[$tzkey]))
5898
			$priority_timezones[$tzvalue['tzid']] = $desc;
5899
		else
5900
			$timezones[$tzvalue['tzid']] = $desc;
5901
	}
5902
5903
	if (!empty($priority_timezones))
5904
		$priority_timezones[] = '-----';
5905
5906
	$timezones = array_merge(
5907
		$priority_timezones,
5908
		array('' => '(Forum Default)', 'UTC' => 'UTC - ' . $tztxt['UTC'], '-----'),
5909
		$timezones
5910
	);
5911
5912
	return $timezones;
5913
}
5914
5915
/**
5916
 * @param string $ip_address An IP address in IPv4, IPv6 or decimal notation
5917
 * @return string|false The IP address in binary or false
5918
 */
5919
function inet_ptod($ip_address)
5920
{
5921
	if (!isValidIP($ip_address))
5922
		return $ip_address;
5923
5924
	$bin = inet_pton($ip_address);
5925
	return $bin;
5926
}
5927
5928
/**
5929
 * @param string $bin An IP address in IPv4, IPv6 (Either string (postgresql) or binary (other databases))
5930
 * @return string|false The IP address in presentation format or false on error
5931
 */
5932
function inet_dtop($bin)
5933
{
5934
	if (empty($bin))
5935
		return '';
5936
5937
	global $db_type;
5938
5939
	if ($db_type == 'postgresql')
5940
		return $bin;
5941
5942
	$ip_address = inet_ntop($bin);
5943
5944
	return $ip_address;
5945
}
5946
5947
/**
5948
 * Safe serialize() and unserialize() replacements
5949
 *
5950
 * @license Public Domain
5951
 *
5952
 * @author anthon (dot) pang (at) gmail (dot) com
5953
 */
5954
5955
/**
5956
 * Safe serialize() replacement. Recursive
5957
 * - output a strict subset of PHP's native serialized representation
5958
 * - does not serialize objects
5959
 *
5960
 * @param mixed $value
5961
 * @return string
5962
 */
5963
function _safe_serialize($value)
5964
{
5965
	if (is_null($value))
5966
		return 'N;';
5967
5968
	if (is_bool($value))
5969
		return 'b:' . (int) $value . ';';
5970
5971
	if (is_int($value))
5972
		return 'i:' . $value . ';';
5973
5974
	if (is_float($value))
5975
		return 'd:' . str_replace(',', '.', $value) . ';';
5976
5977
	if (is_string($value))
5978
		return 's:' . strlen($value) . ':"' . $value . '";';
5979
5980
	if (is_array($value))
5981
	{
5982
		$out = '';
5983
		foreach ($value as $k => $v)
5984
			$out .= _safe_serialize($k) . _safe_serialize($v);
5985
5986
		return 'a:' . count($value) . ':{' . $out . '}';
5987
	}
5988
5989
	// safe_serialize cannot serialize resources or objects.
5990
	return false;
5991
}
5992
5993
/**
5994
 * Wrapper for _safe_serialize() that handles exceptions and multibyte encoding issues.
5995
 *
5996
 * @param mixed $value
5997
 * @return string
5998
 */
5999
function safe_serialize($value)
6000
{
6001
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
6002
	if (function_exists('mb_internal_encoding') &&
6003
		(((int) ini_get('mbstring.func_overload')) & 2))
6004
	{
6005
		$mbIntEnc = mb_internal_encoding();
6006
		mb_internal_encoding('ASCII');
6007
	}
6008
6009
	$out = _safe_serialize($value);
6010
6011
	if (isset($mbIntEnc))
6012
		mb_internal_encoding($mbIntEnc);
6013
6014
	return $out;
6015
}
6016
6017
/**
6018
 * Safe unserialize() replacement
6019
 * - accepts a strict subset of PHP's native serialized representation
6020
 * - does not unserialize objects
6021
 *
6022
 * @param string $str
6023
 * @return mixed
6024
 * @throw Exception if $str is malformed or contains unsupported types (e.g., resources, objects)
6025
 */
6026
function _safe_unserialize($str)
6027
{
6028
	// Input  is not a string.
6029
	if (empty($str) || !is_string($str))
6030
		return false;
6031
6032
	$stack = array();
6033
	$expected = array();
6034
6035
	/*
6036
	 * states:
6037
	 *   0 - initial state, expecting a single value or array
6038
	 *   1 - terminal state
6039
	 *   2 - in array, expecting end of array or a key
6040
	 *   3 - in array, expecting value or another array
6041
	 */
6042
	$state = 0;
6043
	while ($state != 1)
6044
	{
6045
		$type = isset($str[0]) ? $str[0] : '';
6046
		if ($type == '}')
6047
			$str = substr($str, 1);
6048
6049
		elseif ($type == 'N' && $str[1] == ';')
6050
		{
6051
			$value = null;
6052
			$str = substr($str, 2);
6053
		}
6054
		elseif ($type == 'b' && preg_match('/^b:([01]);/', $str, $matches))
6055
		{
6056
			$value = $matches[1] == '1' ? true : false;
6057
			$str = substr($str, 4);
6058
		}
6059
		elseif ($type == 'i' && preg_match('/^i:(-?[0-9]+);(.*)/s', $str, $matches))
6060
		{
6061
			$value = (int) $matches[1];
6062
			$str = $matches[2];
6063
		}
6064
		elseif ($type == 'd' && preg_match('/^d:(-?[0-9]+\.?[0-9]*(E[+-][0-9]+)?);(.*)/s', $str, $matches))
6065
		{
6066
			$value = (float) $matches[1];
6067
			$str = $matches[3];
6068
		}
6069
		elseif ($type == 's' && preg_match('/^s:([0-9]+):"(.*)/s', $str, $matches) && substr($matches[2], (int) $matches[1], 2) == '";')
6070
		{
6071
			$value = substr($matches[2], 0, (int) $matches[1]);
6072
			$str = substr($matches[2], (int) $matches[1] + 2);
6073
		}
6074
		elseif ($type == 'a' && preg_match('/^a:([0-9]+):{(.*)/s', $str, $matches))
6075
		{
6076
			$expectedLength = (int) $matches[1];
6077
			$str = $matches[2];
6078
		}
6079
6080
		// Object or unknown/malformed type.
6081
		else
6082
			return false;
6083
6084
		switch ($state)
6085
		{
6086
			case 3: // In array, expecting value or another array.
6087
				if ($type == 'a')
6088
				{
6089
					$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...
6090
					$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...
6091
					$list = &$list[$key];
6092
					$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...
6093
					$state = 2;
6094
					break;
6095
				}
6096
				if ($type != '}')
6097
				{
6098
					$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...
6099
					$state = 2;
6100
					break;
6101
				}
6102
6103
				// Missing array value.
6104
				return false;
6105
6106
			case 2: // in array, expecting end of array or a key
6107
				if ($type == '}')
6108
				{
6109
					// Array size is less than expected.
6110
					if (count($list) < end($expected))
6111
						return false;
6112
6113
					unset($list);
6114
					$list = &$stack[count($stack) - 1];
6115
					array_pop($stack);
6116
6117
					// Go to terminal state if we're at the end of the root array.
6118
					array_pop($expected);
6119
6120
					if (count($expected) == 0)
6121
						$state = 1;
6122
6123
					break;
6124
				}
6125
6126
				if ($type == 'i' || $type == 's')
6127
				{
6128
					// Array size exceeds expected length.
6129
					if (count($list) >= end($expected))
6130
						return false;
6131
6132
					$key = $value;
6133
					$state = 3;
6134
					break;
6135
				}
6136
6137
				// Illegal array index type.
6138
				return false;
6139
6140
			// Expecting array or value.
6141
			case 0:
6142
				if ($type == 'a')
6143
				{
6144
					$data = array();
6145
					$list = &$data;
6146
					$expected[] = $expectedLength;
6147
					$state = 2;
6148
					break;
6149
				}
6150
6151
				if ($type != '}')
6152
				{
6153
					$data = $value;
6154
					$state = 1;
6155
					break;
6156
				}
6157
6158
				// Not in array.
6159
				return false;
6160
		}
6161
	}
6162
6163
	// Trailing data in input.
6164
	if (!empty($str))
6165
		return false;
6166
6167
	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...
6168
}
6169
6170
/**
6171
 * Wrapper for _safe_unserialize() that handles exceptions and multibyte encoding issue
6172
 *
6173
 * @param string $str
6174
 * @return mixed
6175
 */
6176
function safe_unserialize($str)
6177
{
6178
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
6179
	if (function_exists('mb_internal_encoding') &&
6180
		(((int) ini_get('mbstring.func_overload')) & 0x02))
6181
	{
6182
		$mbIntEnc = mb_internal_encoding();
6183
		mb_internal_encoding('ASCII');
6184
	}
6185
6186
	$out = _safe_unserialize($str);
6187
6188
	if (isset($mbIntEnc))
6189
		mb_internal_encoding($mbIntEnc);
6190
6191
	return $out;
6192
}
6193
6194
/**
6195
 * Tries different modes to make file/dirs writable. Wrapper function for chmod()
6196
 *
6197
 * @param string $file The file/dir full path.
6198
 * @param int $value Not needed, added for legacy reasons.
6199
 * @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.
6200
 */
6201
function smf_chmod($file, $value = 0)
6202
{
6203
	// No file? no checks!
6204
	if (empty($file))
6205
		return false;
6206
6207
	// Already writable?
6208
	if (is_writable($file))
6209
		return true;
6210
6211
	// Do we have a file or a dir?
6212
	$isDir = is_dir($file);
6213
	$isWritable = false;
6214
6215
	// Set different modes.
6216
	$chmodValues = $isDir ? array(0750, 0755, 0775, 0777) : array(0644, 0664, 0666);
6217
6218
	foreach ($chmodValues as $val)
6219
	{
6220
		// If it's writable, break out of the loop.
6221
		if (is_writable($file))
6222
		{
6223
			$isWritable = true;
6224
			break;
6225
		}
6226
6227
		else
6228
			@chmod($file, $val);
6229
	}
6230
6231
	return $isWritable;
6232
}
6233
6234
/**
6235
 * Wrapper function for json_decode() with error handling.
6236
 *
6237
 * @param string $json The string to decode.
6238
 * @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.
6239
 * @param bool $logIt To specify if the error will be logged if theres any.
6240
 * @return array Either an empty array or the decoded data as an array.
6241
 */
6242
function smf_json_decode($json, $returnAsArray = false, $logIt = true)
6243
{
6244
	global $txt;
6245
6246
	// Come on...
6247
	if (empty($json) || !is_string($json))
6248
		return array();
6249
6250
	$returnArray = @json_decode($json, $returnAsArray);
6251
6252
	// PHP 5.3 so no json_last_error_msg()
6253
	switch (json_last_error())
6254
	{
6255
		case JSON_ERROR_NONE:
6256
			$jsonError = false;
6257
			break;
6258
		case JSON_ERROR_DEPTH:
6259
			$jsonError = 'JSON_ERROR_DEPTH';
6260
			break;
6261
		case JSON_ERROR_STATE_MISMATCH:
6262
			$jsonError = 'JSON_ERROR_STATE_MISMATCH';
6263
			break;
6264
		case JSON_ERROR_CTRL_CHAR:
6265
			$jsonError = 'JSON_ERROR_CTRL_CHAR';
6266
			break;
6267
		case JSON_ERROR_SYNTAX:
6268
			$jsonError = 'JSON_ERROR_SYNTAX';
6269
			break;
6270
		case JSON_ERROR_UTF8:
6271
			$jsonError = 'JSON_ERROR_UTF8';
6272
			break;
6273
		default:
6274
			$jsonError = 'unknown';
6275
			break;
6276
	}
6277
6278
	// Something went wrong!
6279
	if (!empty($jsonError) && $logIt)
6280
	{
6281
		// Being a wrapper means we lost our smf_error_handler() privileges :(
6282
		$jsonDebug = debug_backtrace();
6283
		$jsonDebug = $jsonDebug[0];
6284
		loadLanguage('Errors');
6285
6286
		if (!empty($jsonDebug))
6287
			log_error($txt['json_' . $jsonError], 'critical', $jsonDebug['file'], $jsonDebug['line']);
6288
6289
		else
6290
			log_error($txt['json_' . $jsonError], 'critical');
6291
6292
		// Everyone expects an array.
6293
		return array();
6294
	}
6295
6296
	return $returnArray;
6297
}
6298
6299
/**
6300
 * Check the given String if he is a valid IPv4 or IPv6
6301
 * return true or false
6302
 *
6303
 * @param string $IPString
6304
 *
6305
 * @return bool
6306
 */
6307
function isValidIP($IPString)
6308
{
6309
	return filter_var($IPString, FILTER_VALIDATE_IP) !== false;
6310
}
6311
6312
/**
6313
 * Outputs a response.
6314
 * It assumes the data is already a string.
6315
 *
6316
 * @param string $data The data to print
6317
 * @param string $type The content type. Defaults to Json.
6318
 * @return void
6319
 */
6320
function smf_serverResponse($data = '', $type = 'content-type: application/json')
6321
{
6322
	global $db_show_debug, $modSettings;
6323
6324
	// Defensive programming anyone?
6325
	if (empty($data))
6326
		return false;
6327
6328
	// Don't need extra stuff...
6329
	$db_show_debug = false;
6330
6331
	// Kill anything else.
6332
	ob_end_clean();
6333
6334
	if (!empty($modSettings['CompressedOutput']))
6335
		@ob_start('ob_gzhandler');
6336
6337
	else
6338
		ob_start();
6339
6340
	// Set the header.
6341
	header($type);
6342
6343
	// Echo!
6344
	echo $data;
6345
6346
	// Done.
6347
	obExit(false);
6348
}
6349
6350
/**
6351
 * Creates an optimized regex to match all known top level domains.
6352
 *
6353
 * The optimized regex is stored in $modSettings['tld_regex'].
6354
 *
6355
 * To update the stored version of the regex to use the latest list of valid TLDs from iana.org, set
6356
 * the $update parameter to true. Updating can take some time, based on network connectivity, so it
6357
 * should normally only be done by calling this function from a background or scheduled task.
6358
 *
6359
 * If $update is not true, but the regex is missing or invalid, the regex will be regenerated from a
6360
 * hard-coded list of TLDs. This regenerated regex will be overwritten on the next scheduled update.
6361
 *
6362
 * @param bool $update If true, fetch and process the latest official list of TLDs from iana.org.
6363
 */
6364
function set_tld_regex($update = false)
6365
{
6366
	global $sourcedir, $smcFunc, $modSettings;
6367
	static $done = false;
6368
6369
	// If we don't need to do anything, don't
6370
	if (!$update && $done)
6371
		return;
6372
6373
	// Should we get a new copy of the official list of TLDs?
6374
	if ($update)
6375
	{
6376
		$tlds = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt');
6377
6378
		// If the Internet Assigned Numbers Authority can't be reached, the Internet is GONE!
6379
		// We're probably running on a server hidden in a bunker deep underground to protect it from
6380
		// marauding bandits roaming on the surface. We don't want to waste precious electricity on
6381
		// pointlessly repeating background tasks, so we'll wait until the next regularly scheduled
6382
		// update to see if civilization has been restored.
6383
		if ($tlds === false)
6384
			$postapocalypticNightmare = true;
6385
	}
6386
	// If we aren't updating and the regex is valid, we're done
6387
	elseif (!empty($modSettings['tld_regex']) && @preg_match('~' . $modSettings['tld_regex'] . '~', null) !== false)
6388
	{
6389
		$done = true;
6390
		return;
6391
	}
6392
6393
	// If we successfully got an update, process the list into an array
6394
	if (!empty($tlds))
6395
	{
6396
		// Clean $tlds and convert it to an array
6397
		$tlds = array_filter(explode("\n", strtolower($tlds)), function($line)
6398
		{
6399
			$line = trim($line);
6400
			if (empty($line) || strpos($line, '#') !== false || strpos($line, ' ') !== false)
6401
				return false;
6402
			else
6403
				return true;
6404
		});
6405
6406
		// Convert Punycode to Unicode
6407
		require_once($sourcedir . '/Class-Punycode.php');
6408
		$Punycode = new Punycode();
6409
		$tlds = array_map(function($input) use ($Punycode)
6410
		{
6411
			return $Punycode->decode($input);
6412
		}, $tlds);
6413
	}
6414
	// Otherwise, use the 2012 list of gTLDs and ccTLDs for now and schedule a background update
6415
	else
6416
	{
6417
		$tlds = array('com', 'net', 'org', 'edu', 'gov', 'mil', 'aero', 'asia', 'biz', 'cat',
6418
			'coop', 'info', 'int', 'jobs', 'mobi', 'museum', 'name', 'post', 'pro', 'tel',
6419
			'travel', 'xxx', 'ac', 'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'an', 'ao', 'aq',
6420
			'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh',
6421
			'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cc',
6422
			'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 'co', 'cr', 'cs', 'cu', 'cv',
6423
			'cx', 'cy', 'cz', 'dd', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'ee', 'eg', 'eh',
6424
			'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr', 'ga', 'gb', 'gd', 'ge',
6425
			'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw',
6426
			'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu', 'id', 'ie', 'il', 'im', 'in', 'io', 'iq',
6427
			'ir', 'is', 'it', 'ja', 'je', 'jm', 'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn',
6428
			'kp', 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu',
6429
			'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp',
6430
			'mq', 'mr', 'ms', 'mt', 'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf',
6431
			'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph',
6432
			'pk', 'pl', 'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru',
6433
			'rw', 'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn',
6434
			'so', 'sr', 'ss', 'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th',
6435
			'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tp', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug',
6436
			'uk', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', 'ws', 'ye',
6437
			'yt', 'yu', 'za', 'zm', 'zw');
6438
6439
		// Schedule a background update, unless civilization has collapsed and/or we are having connectivity issues.
6440
		if (empty($postapocalypticNightmare))
6441
		{
6442
			$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
6443
				array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
6444
				array('$sourcedir/tasks/UpdateTldRegex.php', 'Update_TLD_Regex', '', 0), array()
6445
			);
6446
		}
6447
	}
6448
6449
	// Get an optimized regex to match all the TLDs
6450
	$tld_regex = build_regex($tlds);
6451
6452
	// Remember the new regex in $modSettings
6453
	updateSettings(array('tld_regex' => $tld_regex));
6454
6455
	// Redundant repetition is redundant
6456
	$done = true;
6457
}
6458
6459
/**
6460
 * Creates optimized regular expressions from an array of strings.
6461
 *
6462
 * An optimized regex built using this function will be much faster than a simple regex built using
6463
 * `implode('|', $strings)` --- anywhere from several times to several orders of magnitude faster.
6464
 *
6465
 * However, the time required to build the optimized regex is approximately equal to the time it
6466
 * takes to execute the simple regex. Therefore, it is only worth calling this function if the
6467
 * resulting regex will be used more than once.
6468
 *
6469
 * Because PHP places an upper limit on the allowed length of a regex, very large arrays of $strings
6470
 * may not fit in a single regex. Normally, the excess strings will simply be dropped. However, if
6471
 * the $returnArray parameter is set to true, this function will build as many regexes as necessary
6472
 * to accommodate everything in $strings and return them in an array. You will need to iterate
6473
 * through all elements of the returned array in order to test all possible matches.
6474
 *
6475
 * @param array $strings An array of strings to make a regex for.
6476
 * @param string $delim An optional delimiter character to pass to preg_quote().
6477
 * @param bool $returnArray If true, returns an array of regexes.
6478
 * @return string|array One or more regular expressions to match any of the input strings.
6479
 */
6480
function build_regex($strings, $delim = null, $returnArray = false)
6481
{
6482
	global $smcFunc;
6483
6484
	// The mb_* functions are faster than the $smcFunc ones, but may not be available
6485
	if (function_exists('mb_internal_encoding') && function_exists('mb_detect_encoding') && function_exists('mb_strlen') && function_exists('mb_substr'))
6486
	{
6487
		if (($string_encoding = mb_detect_encoding(implode(' ', $strings))) !== false)
6488
		{
6489
			$current_encoding = mb_internal_encoding();
6490
			mb_internal_encoding($string_encoding);
6491
		}
6492
6493
		$strlen = 'mb_strlen';
6494
		$substr = 'mb_substr';
6495
	}
6496
	else
6497
	{
6498
		$strlen = $smcFunc['strlen'];
6499
		$substr = $smcFunc['substr'];
6500
	}
6501
6502
	// This recursive function creates the index array from the strings
6503
	$add_string_to_index = function($string, $index) use (&$strlen, &$substr, &$add_string_to_index)
6504
	{
6505
		static $depth = 0;
6506
		$depth++;
6507
6508
		$first = $substr($string, 0, 1);
6509
6510
		if (empty($index[$first]))
6511
			$index[$first] = array();
6512
6513
		if ($strlen($string) > 1)
6514
		{
6515
			// Sanity check on recursion
6516
			if ($depth > 99)
6517
				$index[$first][$substr($string, 1)] = '';
6518
6519
			else
6520
				$index[$first] = $add_string_to_index($substr($string, 1), $index[$first]);
6521
		}
6522
		else
6523
			$index[$first][''] = '';
6524
6525
		$depth--;
6526
		return $index;
6527
	};
6528
6529
	// This recursive function turns the index array into a regular expression
6530
	$index_to_regex = function(&$index, $delim) use (&$strlen, &$index_to_regex)
6531
	{
6532
		static $depth = 0;
6533
		$depth++;
6534
6535
		// Absolute max length for a regex is 32768, but we might need wiggle room
6536
		$max_length = 30000;
6537
6538
		$regex = array();
6539
		$length = 0;
6540
6541
		foreach ($index as $key => $value)
6542
		{
6543
			$key_regex = preg_quote($key, $delim);
6544
			$new_key = $key;
6545
6546
			if (empty($value))
6547
				$sub_regex = '';
6548
			else
6549
			{
6550
				$sub_regex = $index_to_regex($value, $delim);
6551
6552
				if (count(array_keys($value)) == 1)
6553
				{
6554
					$new_key_array = explode('(?' . '>', $sub_regex);
6555
					$new_key .= $new_key_array[0];
6556
				}
6557
				else
6558
					$sub_regex = '(?' . '>' . $sub_regex . ')';
6559
			}
6560
6561
			if ($depth > 1)
6562
				$regex[$new_key] = $key_regex . $sub_regex;
6563
			else
6564
			{
6565
				if (($length += strlen($key_regex) + 1) < $max_length || empty($regex))
6566
				{
6567
					$regex[$new_key] = $key_regex . $sub_regex;
6568
					unset($index[$key]);
6569
				}
6570
				else
6571
					break;
6572
			}
6573
		}
6574
6575
		// Sort by key length and then alphabetically
6576
		uksort($regex, function($k1, $k2) use (&$strlen)
6577
		{
6578
			$l1 = $strlen($k1);
6579
			$l2 = $strlen($k2);
6580
6581
			if ($l1 == $l2)
6582
				return strcmp($k1, $k2) > 0 ? 1 : -1;
6583
			else
6584
				return $l1 > $l2 ? -1 : 1;
6585
		});
6586
6587
		$depth--;
6588
		return implode('|', $regex);
6589
	};
6590
6591
	// Now that the functions are defined, let's do this thing
6592
	$index = array();
6593
	$regex = '';
6594
6595
	foreach ($strings as $string)
6596
		$index = $add_string_to_index($string, $index);
6597
6598
	if ($returnArray === true)
6599
	{
6600
		$regex = array();
6601
		while (!empty($index))
6602
			$regex[] = '(?' . '>' . $index_to_regex($index, $delim) . ')';
6603
	}
6604
	else
6605
		$regex = '(?' . '>' . $index_to_regex($index, $delim) . ')';
6606
6607
	// Restore PHP's internal character encoding to whatever it was originally
6608
	if (!empty($current_encoding))
6609
		mb_internal_encoding($current_encoding);
6610
6611
	return $regex;
6612
}
6613
6614
/**
6615
 * Check if the passed url has an SSL certificate.
6616
 *
6617
 * Returns true if a cert was found & false if not.
6618
 *
6619
 * @param string $url to check, in $boardurl format (no trailing slash).
6620
 */
6621
function ssl_cert_found($url)
6622
{
6623
	// This check won't work without OpenSSL
6624
	if (!extension_loaded('openssl'))
6625
		return true;
6626
6627
	// First, strip the subfolder from the passed url, if any
6628
	$parsedurl = parse_url($url);
6629
	$url = 'ssl://' . $parsedurl['host'] . ':443';
6630
6631
	// Next, check the ssl stream context for certificate info
6632
	if (version_compare(PHP_VERSION, '5.6.0', '<'))
6633
		$ssloptions = array("capture_peer_cert" => true);
6634
	else
6635
		$ssloptions = array("capture_peer_cert" => true, "verify_peer" => true, "allow_self_signed" => true);
6636
6637
	$result = false;
6638
	$context = stream_context_create(array("ssl" => $ssloptions));
6639
	$stream = @stream_socket_client($url, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
6640
	if ($stream !== false)
6641
	{
6642
		$params = stream_context_get_params($stream);
6643
		$result = isset($params["options"]["ssl"]["peer_certificate"]) ? true : false;
6644
	}
6645
	return $result;
6646
}
6647
6648
/**
6649
 * Check if the passed url has a redirect to https:// by querying headers.
6650
 *
6651
 * Returns true if a redirect was found & false if not.
6652
 * Note that when force_ssl = 2, SMF issues its own redirect...  So if this
6653
 * returns true, it may be caused by SMF, not necessarily an .htaccess redirect.
6654
 *
6655
 * @param string $url to check, in $boardurl format (no trailing slash).
6656
 */
6657
function https_redirect_active($url)
6658
{
6659
	// Ask for the headers for the passed url, but via http...
6660
	// Need to add the trailing slash, or it puts it there & thinks there's a redirect when there isn't...
6661
	$url = str_ireplace('https://', 'http://', $url) . '/';
6662
	$headers = @get_headers($url);
6663
	if ($headers === false)
6664
		return false;
6665
6666
	// Now to see if it came back https...
6667
	// First check for a redirect status code in first row (301, 302, 307)
6668
	if (strstr($headers[0], '301') === false && strstr($headers[0], '302') === false && strstr($headers[0], '307') === false)
6669
		return false;
6670
6671
	// Search for the location entry to confirm https
6672
	$result = false;
6673
	foreach ($headers as $header)
6674
	{
6675
		if (stristr($header, 'Location: https://') !== false)
6676
		{
6677
			$result = true;
6678
			break;
6679
		}
6680
	}
6681
	return $result;
6682
}
6683
6684
/**
6685
 * Build query_wanna_see_board and query_see_board for a userid
6686
 *
6687
 * Returns array with keys query_wanna_see_board and query_see_board
6688
 *
6689
 * @param int $userid of the user
6690
 */
6691
function build_query_board($userid)
6692
{
6693
	global $user_info, $modSettings, $smcFunc, $db_prefix;
6694
6695
	$query_part = array();
6696
6697
	// If we come from cron, we can't have a $user_info.
6698
	if (isset($user_info['id']) && $user_info['id'] == $userid)
6699
	{
6700
		$groups = $user_info['groups'];
6701
		$can_see_all_boards = $user_info['is_admin'] || $user_info['can_manage_boards'];
6702
		$ignoreboards = !empty($user_info['ignoreboards']) ? $user_info['ignoreboards'] : null;
6703
	}
6704
	else
6705
	{
6706
		$request = $smcFunc['db_query']('', '
6707
			SELECT mem.ignore_boards, mem.id_group, mem.additional_groups, mem.id_post_group
6708
			FROM {db_prefix}members AS mem
6709
			WHERE mem.id_member = {int:id_member}
6710
			LIMIT 1',
6711
			array(
6712
				'id_member' => $userid,
6713
			)
6714
		);
6715
6716
		$row = $smcFunc['db_fetch_assoc']($request);
6717
6718
		if (empty($row['additional_groups']))
6719
			$groups = array($row['id_group'], $row['id_post_group']);
6720
		else
6721
			$groups = array_merge(
6722
				array($row['id_group'], $row['id_post_group']),
6723
				explode(',', $row['additional_groups'])
6724
			);
6725
6726
		// Because history has proven that it is possible for groups to go bad - clean up in case.
6727
		foreach ($groups as $k => $v)
6728
			$groups[$k] = (int) $v;
6729
6730
		$can_see_all_boards = in_array(1, $groups) || (!empty($modSettings['board_manager_groups']) && count(array_intersect($groups, explode(',', $modSettings['board_manager_groups']))) > 0);
6731
6732
		$ignoreboards = !empty($row['ignore_boards']) && !empty($modSettings['allow_ignore_boards']) ? explode(',', $row['ignore_boards']) : array();
6733
	}
6734
6735
	// Just build this here, it makes it easier to change/use - administrators can see all boards.
6736
	if ($can_see_all_boards)
6737
		$query_part['query_see_board'] = '1=1';
6738
	// Otherwise just the groups in $user_info['groups'].
6739
	else
6740
	{
6741
		$query_part['query_see_board'] = '
6742
			EXISTS (
6743
				SELECT bpv.id_board
6744
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
6745
				WHERE bpv.id_group IN ('. implode(',', $groups) .')
6746
					AND bpv.deny = 0
6747
					AND bpv.id_board = b.id_board
6748
			)';
6749
6750
		if (!empty($modSettings['deny_boards_access']))
6751
			$query_part['query_see_board'] .= '
6752
			AND NOT EXISTS (
6753
				SELECT bpv.id_board
6754
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
6755
				WHERE bpv.id_group IN ( '. implode(',', $groups) .')
6756
					AND bpv.deny = 1
6757
					AND bpv.id_board = b.id_board
6758
			)';
6759
	}
6760
6761
	// Build the list of boards they WANT to see.
6762
	// This will take the place of query_see_boards in certain spots, so it better include the boards they can see also
6763
6764
	// If they aren't ignoring any boards then they want to see all the boards they can see
6765
	if (empty($ignoreboards))
6766
		$query_part['query_wanna_see_board'] = $query_part['query_see_board'];
6767
	// Ok I guess they don't want to see all the boards
6768
	else
6769
		$query_part['query_wanna_see_board'] = '(' . $query_part['query_see_board'] . ' AND b.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
6770
6771
	$query_part['query_see_message_board'] = str_replace('b.', 'm.', $query_part['query_see_board']);
6772
	$query_part['query_see_topic_board'] = str_replace('b.', 't.', $query_part['query_see_board']);
6773
6774
	return $query_part;
6775
}
6776
6777
/**
6778
 * Check if the connection is using https.
6779
 *
6780
 * @return boolean true if connection used https
6781
 */
6782
function httpsOn()
6783
{
6784
	$secure = false;
6785
6786
	if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on')
6787
		$secure = true;
6788
	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...
6789
		$secure = true;
6790
6791
	return $secure;
6792
}
6793
6794
/**
6795
 * A wrapper for `filter_var($url, FILTER_VALIDATE_URL)` that can handle URLs
6796
 * with international characters (a.k.a. IRIs)
6797
 *
6798
 * @param string $iri The IRI to test.
6799
 * @param int $flags Optional flags to pass to filter_var()
6800
 * @return string|bool Either the original IRI, or false if the IRI was invalid.
6801
 */
6802
function validate_iri($iri, $flags = null)
6803
{
6804
	$url = iri_to_url($iri);
6805
6806
	if (filter_var($url, FILTER_VALIDATE_URL, $flags) !== false)
6807
		return $iri;
6808
	else
6809
		return false;
6810
}
6811
6812
/**
6813
 * A wrapper for `filter_var($url, FILTER_SANITIZE_URL)` that can handle URLs
6814
 * with international characters (a.k.a. IRIs)
6815
 *
6816
 * Note: The returned value will still be an IRI, not a URL. To convert to URL,
6817
 * feed the result of this function to iri_to_url()
6818
 *
6819
 * @param string $iri The IRI to sanitize.
6820
 * @return string|bool The sanitized version of the IRI
6821
 */
6822
function sanitize_iri($iri)
6823
{
6824
	// Encode any non-ASCII characters (but not space or control characters of any sort)
6825
	$iri = preg_replace_callback('~[^\x00-\x7F\pZ\pC]~u', function($matches)
6826
	{
6827
		return rawurlencode($matches[0]);
6828
	}, $iri);
6829
6830
	// Perform normal sanitization
6831
	$iri = filter_var($iri, FILTER_SANITIZE_URL);
6832
6833
	// Decode the non-ASCII characters
6834
	$iri = rawurldecode($iri);
6835
6836
	return $iri;
6837
}
6838
6839
/**
6840
 * Converts a URL with international characters (an IRI) into a pure ASCII URL
6841
 *
6842
 * Uses Punycode to encode any non-ASCII characters in the domain name, and uses
6843
 * standard URL encoding on the rest.
6844
 *
6845
 * @param string $iri A IRI that may or may not contain non-ASCII characters.
6846
 * @return string|bool The URL version of the IRI.
6847
 */
6848
function iri_to_url($iri)
6849
{
6850
	global $sourcedir;
6851
6852
	$host = parse_url((strpos($iri, '://') === false ? 'http://' : '') . ltrim($iri, ':/'), PHP_URL_HOST);
6853
6854
	if (empty($host))
6855
		return $iri;
6856
6857
	// Convert the domain using the Punycode algorithm
6858
	require_once($sourcedir . '/Class-Punycode.php');
6859
	$Punycode = new Punycode();
6860
	$encoded_host = $Punycode->encode($host);
6861
	$pos = strpos($iri, $host);
6862
	$iri = substr_replace($iri, $encoded_host, $pos, strlen($host));
6863
6864
	// Encode any disallowed characters in the rest of the URL
6865
	$unescaped = array(
6866
		'%21' => '!', '%23' => '#', '%24' => '$', '%26' => '&',
6867
		'%27' => "'", '%28' => '(', '%29' => ')', '%2A' => '*',
6868
		'%2B' => '+', '%2C' => ',', '%2F' => '/', '%3A' => ':',
6869
		'%3B' => ';', '%3D' => '=', '%3F' => '?', '%40' => '@',
6870
		'%25' => '%',
6871
	);
6872
	$iri = strtr(rawurlencode($iri), $unescaped);
6873
6874
	return $iri;
6875
}
6876
6877
/**
6878
 * Decodes a URL containing encoded international characters to UTF-8
6879
 *
6880
 * Decodes any Punycode encoded characters in the domain name, then uses
6881
 * standard URL decoding on the rest.
6882
 *
6883
 * @param string $url The pure ASCII version of a URL.
6884
 * @return string|bool The UTF-8 version of the URL.
6885
 */
6886
function url_to_iri($url)
6887
{
6888
	global $sourcedir;
6889
6890
	$host = parse_url((strpos($url, '://') === false ? 'http://' : '') . ltrim($url, ':/'), PHP_URL_HOST);
6891
6892
	if (empty($host))
6893
		return $url;
6894
6895
	// Decode the domain from Punycode
6896
	require_once($sourcedir . '/Class-Punycode.php');
6897
	$Punycode = new Punycode();
6898
	$decoded_host = $Punycode->decode($host);
6899
	$pos = strpos($url, $host);
6900
	$url = substr_replace($url, $decoded_host, $pos, strlen($host));
6901
6902
	// Decode the rest of the URL
6903
	$url = rawurldecode($url);
6904
6905
	return $url;
6906
}
6907
6908
/**
6909
 * Ensures SMF's scheduled tasks are being run as intended
6910
 *
6911
 * If the admin activated the cron_is_real_cron setting, but the cron job is
6912
 * not running things at least once per day, we need to go back to SMF's default
6913
 * behaviour using "web cron" JavaScript calls.
6914
 */
6915
function check_cron()
6916
{
6917
	global $user_info, $modSettings, $smcFunc, $txt;
6918
6919
	if (empty($modSettings['cron_last_checked']))
6920
		$modSettings['cron_last_checked'] = 0;
6921
6922
	if (!empty($modSettings['cron_is_real_cron']) && time() - $modSettings['cron_last_checked'] > 84600)
6923
	{
6924
		$request = $smcFunc['db_query']('', '
6925
			SELECT time_run
6926
			FROM {db_prefix}log_scheduled_tasks
6927
			ORDER BY id_log DESC
6928
			LIMIT 1',
6929
			array()
6930
		);
6931
		list($time_run) = $smcFunc['db_fetch_row']($request);
6932
		$smcFunc['db_free_result']($request);
6933
6934
		// If it's been more than 24 hours since the last task ran, cron must not be working
6935
		if (!empty($time_run) && time() - $time_run > 84600)
6936
		{
6937
			loadLanguage('ManageScheduledTasks');
6938
			log_error($txt['cron_not_working']);
6939
			updateSettings(array('cron_is_real_cron' => 0));
6940
		}
6941
		else
6942
			updateSettings(array('cron_last_checked' => time()));
6943
	}
6944
}
6945
6946
/**
6947
 * Sends an appropriate HTTP status header based on a given status code
6948
 *
6949
 * @param int $code The status code
6950
 * @param string $status The string for the status. Set automatically if not provided.
6951
 */
6952
function send_http_status($code, $status = '')
6953
{
6954
	$statuses = array(
6955
		206 => 'Partial Content',
6956
		304 => 'Not Modified',
6957
		400 => 'Bad Request',
6958
		403 => 'Forbidden',
6959
		404 => 'Not Found',
6960
		410 => 'Gone',
6961
		500 => 'Internal Server Error',
6962
		503 => 'Service Unavailable',
6963
	);
6964
6965
	$protocol = preg_match('~^\s*(HTTP/[12]\.\d)\s*$~i', $_SERVER['SERVER_PROTOCOL'], $matches) ? $matches[1] : 'HTTP/1.0';
6966
6967
	if (!isset($statuses[$code]) && empty($status))
6968
		header($protocol . ' 500 Internal Server Error');
6969
	else
6970
		header($protocol . ' ' . $code . ' ' . (!empty($status) ? $status : $statuses[$code]));
6971
}
6972
6973
/**
6974
 * Concatenates an array of strings into a grammatically correct sentence list
6975
 *
6976
 * Uses formats defined in the language files to build the list appropropriately
6977
 * for the currently loaded language.
6978
 *
6979
 * @param array $list An array of strings to concatenate.
6980
 * @return string The localized sentence list.
6981
 */
6982
function sentence_list($list)
6983
{
6984
	global $txt;
6985
6986
	// Make sure the bare necessities are defined
6987
	if (empty($txt['sentence_list_format']['n']))
6988
		$txt['sentence_list_format']['n'] = '{series}';
6989
	if (!isset($txt['sentence_list_separator']))
6990
		$txt['sentence_list_separator'] = ', ';
6991
	if (!isset($txt['sentence_list_separator_alt']))
6992
		$txt['sentence_list_separator_alt'] = '; ';
6993
6994
	// Which format should we use?
6995
	if (isset($txt['sentence_list_format'][count($list)]))
6996
		$format = $txt['sentence_list_format'][count($list)];
6997
	else
6998
		$format = $txt['sentence_list_format']['n'];
6999
7000
	// Do we want the normal separator or the alternate?
7001
	$separator = $txt['sentence_list_separator'];
7002
	foreach ($list as $item)
7003
	{
7004
		if (strpos($item, $separator) !== false)
7005
		{
7006
			$separator = $txt['sentence_list_separator_alt'];
7007
			$format = strtr($format, trim($txt['sentence_list_separator']), trim($separator));
7008
			break;
7009
		}
7010
	}
7011
7012
	$replacements = array();
7013
7014
	// Special handling for the last items on the list
7015
	$i = 0;
7016
	while (empty($done))
7017
	{
7018
		if (strpos($format, '{'. --$i . '}') !== false)
7019
			$replacements['{'. $i . '}'] = array_pop($list);
7020
		else
7021
			$done = true;
7022
	}
7023
	unset($done);
7024
7025
	// Special handling for the first items on the list
7026
	$i = 0;
7027
	while (empty($done))
7028
	{
7029
		if (strpos($format, '{'. ++$i . '}') !== false)
7030
			$replacements['{'. $i . '}'] = array_shift($list);
7031
		else
7032
			$done = true;
7033
	}
7034
	unset($done);
7035
7036
	// Whatever is left
7037
	$replacements['{series}'] = implode($separator, $list);
7038
7039
	// Do the deed
7040
	return strtr($format, $replacements);
7041
}
7042
7043
?>