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

call_integration_hook()   C

Complexity

Conditions 12

Size

Total Lines 51
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 12
eloc 27
nop 2
dl 0
loc 51
rs 6.9666
c 1
b 1
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 RC1
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)
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_cache;
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
		$time = $log_time + ($user_info['time_offset'] + $modSettings['time_offset']) * 3600;
754
	// Just the forum offset?
755
	elseif ($offset_type == 'forum')
756
		$time = $log_time + $modSettings['time_offset'] * 3600;
757
	else
758
		$time = $log_time;
759
760
	// We can't have a negative date (on Windows, at least.)
761
	if ($log_time < 0)
762
		$log_time = 0;
763
764
	// Today and Yesterday?
765
	if ($modSettings['todayMod'] >= 1 && $show_today === true)
766
	{
767
		// Get the current time.
768
		$nowtime = forum_time();
769
770
		$then = @getdate($time);
771
		$now = @getdate($nowtime);
772
773
		// Try to make something of a time format string...
774
		$s = strpos($user_info['time_format'], '%S') === false ? '' : ':%S';
775
		if (strpos($user_info['time_format'], '%H') === false && strpos($user_info['time_format'], '%T') === false)
776
		{
777
			$h = strpos($user_info['time_format'], '%l') === false ? '%I' : '%l';
778
			$today_fmt = $h . ':%M' . $s . ' %p';
779
		}
780
		else
781
			$today_fmt = '%H:%M' . $s;
782
783
		// Same day of the year, same year.... Today!
784
		if ($then['yday'] == $now['yday'] && $then['year'] == $now['year'])
785
			return $txt['today'] . timeformat($log_time, $today_fmt, $offset_type);
0 ignored issues
show
Bug introduced by
$today_fmt of type string is incompatible with the type boolean expected by parameter $show_today of timeformat(). ( Ignorable by Annotation )

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

785
			return $txt['today'] . timeformat($log_time, /** @scrutinizer ignore-type */ $today_fmt, $offset_type);
Loading history...
786
787
		// 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...
788
		if ($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))
789
			return $txt['yesterday'] . timeformat($log_time, $today_fmt, $offset_type);
790
	}
791
792
	$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...
793
794
	// Use the cached formats if available
795
	if (is_null($finalizedFormats))
796
		$finalizedFormats = (array) cache_get_data('timeformatstrings', 86400);
797
798
	// Make a supported version for this format if we don't already have one
799
	if (empty($finalizedFormats[$str]))
800
	{
801
		$timeformat = $str;
802
803
		// Not all systems support all formats, and Windows fails altogether if unsupported ones are
804
		// used, so let's prevent that. Some substitutions go to the nearest reasonable fallback, some
805
		// turn into static strings, some (i.e. %a, %A, $b, %B, %p) have special handling below.
806
		$strftimeFormatSubstitutions = array(
807
			// Day
808
			'a' => '%a', 'A' => '%A', 'e' => '%d', 'd' => '&#37;d', 'j' => '&#37;j', 'u' => '%w', 'w' => '&#37;w',
809
			// Week
810
			'U' => '&#37;U', 'V' => '%U', 'W' => '%U',
811
			// Month
812
			'b' => '%b', 'B' => '%B', 'h' => '%b', 'm' => '%b',
813
			// Year
814
			'C' => '&#37;C', 'g' => '%y', 'G' => '%Y', 'y' => '&#37;y', 'Y' => '&#37;Y',
815
			// Time
816
			'H' => '&#37;H', 'k' => '%H', 'I' => '%H', 'l' => '%I', 'M' => '&#37;M', 'p' => '%p', 'P' => '%p',
817
			'r' => '%I:%M:%S %p', 'R' => '%H:%M', 'S' => '&#37;S', 'T' => '%H:%M:%S', 'X' => '%T', 'z' => '&#37;z', 'Z' => '&#37;Z',
818
			// Time and Date Stamps
819
			'c' => '%F %T', 'D' => '%m/%d/%y', 'F' => '%Y-%m-%d', 's' => '&#37;s', 'x' => '%F',
820
			// Miscellaneous
821
			'n' => "\n", 't' => "\t", '%' => '&#37;',
822
		);
823
824
		// No need to do this part again if we already did it once
825
		if (is_null($unsupportedFormats))
826
			$unsupportedFormats = (array) cache_get_data('unsupportedtimeformats', 86400);
827
		if (empty($unsupportedFormats))
828
		{
829
			foreach ($strftimeFormatSubstitutions as $format => $substitution)
830
			{
831
				// Avoid a crashing bug with PHP 7 on certain versions of Windows
832
				if ($context['server']['is_windows'] && in_array($format, $unsupportedFormatsWindows))
833
				{
834
					$unsupportedFormats[] = $format;
835
					continue;
836
				}
837
838
				$value = @strftime('%' . $format);
839
840
				// Windows will return false for unsupported formats
841
				// Other operating systems return the format string as a literal
842
				if ($value === false || $value === $format)
843
					$unsupportedFormats[] = $format;
844
			}
845
			cache_put_data('unsupportedtimeformats', $unsupportedFormats, 86400);
846
		}
847
848
		// Windows needs extra help if $timeformat contains something completely invalid, e.g. '%Q'
849
		if (DIRECTORY_SEPARATOR === '\\')
850
			$timeformat = preg_replace('~%(?!' . implode('|', array_keys($strftimeFormatSubstitutions)) . ')~', '&#37;', $timeformat);
851
852
		// Substitute unsupported formats with supported ones
853
		if (!empty($unsupportedFormats))
854
			while (preg_match('~%(' . implode('|', $unsupportedFormats) . ')~', $timeformat, $matches))
855
				$timeformat = str_replace($matches[0], $strftimeFormatSubstitutions[$matches[1]], $timeformat);
856
857
		// Remember this so we don't need to do it again
858
		$finalizedFormats[$str] = $timeformat;
859
		cache_put_data('timeformatstrings', $finalizedFormats, 86400);
860
	}
861
862
	$str = $finalizedFormats[$str];
863
864
	if (!isset($locale_cache))
865
		$locale_cache = setlocale(LC_TIME, $txt['lang_locale'] . !empty($modSettings['global_character_set']) ? '.' . $modSettings['global_character_set'] : '');
866
867
	if ($locale_cache !== false)
868
	{
869
		// Check if another process changed the locale
870
		if ($process_safe === true && setlocale(LC_TIME, '0') != $locale_cache)
871
			setlocale(LC_TIME, $txt['lang_locale'] . !empty($modSettings['global_character_set']) ? '.' . $modSettings['global_character_set'] : '');
872
873
		if (!isset($non_twelve_hour))
874
			$non_twelve_hour = trim(strftime('%p')) === '';
875
		if ($non_twelve_hour && strpos($str, '%p') !== false)
876
			$str = str_replace('%p', (strftime('%H', $time) < 12 ? $txt['time_am'] : $txt['time_pm']), $str);
877
878
		foreach (array('%a', '%A', '%b', '%B') as $token)
879
			if (strpos($str, $token) !== false)
880
				$str = str_replace($token, strftime($token, $time), $str);
881
	}
882
	else
883
	{
884
		// Do-it-yourself time localization.  Fun.
885
		foreach (array('%a' => 'days_short', '%A' => 'days', '%b' => 'months_short', '%B' => 'months') as $token => $text_label)
886
			if (strpos($str, $token) !== false)
887
				$str = str_replace($token, $txt[$text_label][(int) strftime($token === '%a' || $token === '%A' ? '%w' : '%m', $time)], $str);
888
889
		if (strpos($str, '%p') !== false)
890
			$str = str_replace('%p', (strftime('%H', $time) < 12 ? $txt['time_am'] : $txt['time_pm']), $str);
891
	}
892
893
	// Format the time and then restore any literal percent characters
894
	return str_replace('&#37;', '%', strftime($str, $time));
895
}
896
897
/**
898
 * Replaces special entities in strings with the real characters.
899
 *
900
 * Functionally equivalent to htmlspecialchars_decode(), except that this also
901
 * replaces '&nbsp;' with a simple space character.
902
 *
903
 * @param string $string A string
904
 * @return string The string without entities
905
 */
906
function un_htmlspecialchars($string)
907
{
908
	global $context;
909
	static $translation = array();
910
911
	// Determine the character set... Default to UTF-8
912
	if (empty($context['character_set']))
913
		$charset = 'UTF-8';
914
	// Use ISO-8859-1 in place of non-supported ISO-8859 charsets...
915
	elseif (strpos($context['character_set'], 'ISO-8859-') !== false && !in_array($context['character_set'], array('ISO-8859-5', 'ISO-8859-15')))
916
		$charset = 'ISO-8859-1';
917
	else
918
		$charset = $context['character_set'];
919
920
	if (empty($translation))
921
		$translation = array_flip(get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES, $charset)) + array('&#039;' => '\'', '&#39;' => '\'', '&nbsp;' => ' ');
922
923
	return strtr($string, $translation);
924
}
925
926
/**
927
 * Shorten a subject + internationalization concerns.
928
 *
929
 * - shortens a subject so that it is either shorter than length, or that length plus an ellipsis.
930
 * - respects internationalization characters and entities as one character.
931
 * - avoids trailing entities.
932
 * - returns the shortened string.
933
 *
934
 * @param string $subject The subject
935
 * @param int $len How many characters to limit it to
936
 * @return string The shortened subject - either the entire subject (if it's <= $len) or the subject shortened to $len characters with "..." appended
937
 */
938
function shorten_subject($subject, $len)
939
{
940
	global $smcFunc;
941
942
	// It was already short enough!
943
	if ($smcFunc['strlen']($subject) <= $len)
944
		return $subject;
945
946
	// Shorten it by the length it was too long, and strip off junk from the end.
947
	return $smcFunc['substr']($subject, 0, $len) . '...';
948
}
949
950
/**
951
 * Gets the current time with offset.
952
 *
953
 * - always applies the offset in the time_offset setting.
954
 *
955
 * @param bool $use_user_offset Whether to apply the user's offset as well
956
 * @param int $timestamp A timestamp (null to use current time)
957
 * @return int Seconds since the unix epoch, with forum time offset and (optionally) user time offset applied
958
 */
959
function forum_time($use_user_offset = true, $timestamp = null)
960
{
961
	global $user_info, $modSettings;
962
963
	if ($timestamp === null)
964
		$timestamp = time();
965
	elseif ($timestamp == 0)
966
		return 0;
967
968
	return $timestamp + ($modSettings['time_offset'] + ($use_user_offset ? $user_info['time_offset'] : 0)) * 3600;
969
}
970
971
/**
972
 * Calculates all the possible permutations (orders) of array.
973
 * should not be called on huge arrays (bigger than like 10 elements.)
974
 * returns an array containing each permutation.
975
 *
976
 * @deprecated since 2.1
977
 * @param array $array An array
978
 * @return array An array containing each permutation
979
 */
980
function permute($array)
981
{
982
	$orders = array($array);
983
984
	$n = count($array);
985
	$p = range(0, $n);
986
	for ($i = 1; $i < $n; null)
987
	{
988
		$p[$i]--;
989
		$j = $i % 2 != 0 ? $p[$i] : 0;
990
991
		$temp = $array[$i];
992
		$array[$i] = $array[$j];
993
		$array[$j] = $temp;
994
995
		for ($i = 1; $p[$i] == 0; $i++)
996
			$p[$i] = 1;
997
998
		$orders[] = $array;
999
	}
1000
1001
	return $orders;
1002
}
1003
1004
/**
1005
 * Parse bulletin board code in a string, as well as smileys optionally.
1006
 *
1007
 * - only parses bbc tags which are not disabled in disabledBBC.
1008
 * - handles basic HTML, if enablePostHTML is on.
1009
 * - caches the from/to replace regular expressions so as not to reload them every time a string is parsed.
1010
 * - only parses smileys if smileys is true.
1011
 * - does nothing if the enableBBC setting is off.
1012
 * - uses the cache_id as a unique identifier to facilitate any caching it may do.
1013
 * - returns the modified message.
1014
 *
1015
 * @param string $message The message
1016
 * @param bool $smileys Whether to parse smileys as well
1017
 * @param string $cache_id The cache ID
1018
 * @param array $parse_tags If set, only parses these tags rather than all of them
1019
 * @return string The parsed message
1020
 */
1021
function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = array())
1022
{
1023
	global $smcFunc, $txt, $scripturl, $context, $modSettings, $user_info, $sourcedir;
1024
	static $bbc_codes = array(), $itemcodes = array(), $no_autolink_tags = array();
1025
	static $disabled, $alltags_regex = '', $param_regexes = array();
1026
1027
	// Don't waste cycles
1028
	if ($message === '')
1029
		return '';
1030
1031
	// Just in case it wasn't determined yet whether UTF-8 is enabled.
1032
	if (!isset($context['utf8']))
1033
		$context['utf8'] = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8';
1034
1035
	// Clean up any cut/paste issues we may have
1036
	$message = sanitizeMSCutPaste($message);
1037
1038
	// If the load average is too high, don't parse the BBC.
1039
	if (!empty($context['load_average']) && !empty($modSettings['bbc']) && $context['load_average'] >= $modSettings['bbc'])
1040
	{
1041
		$context['disabled_parse_bbc'] = true;
1042
		return $message;
1043
	}
1044
1045
	if ($smileys !== null && ($smileys == '1' || $smileys == '0'))
1046
		$smileys = (bool) $smileys;
1047
1048
	if (empty($modSettings['enableBBC']) && $message !== false)
1049
	{
1050
		if ($smileys === true)
1051
			parsesmileys($message);
1052
1053
		return $message;
1054
	}
1055
1056
	// If we are not doing every tag then we don't cache this run.
1057
	if (!empty($parse_tags) && !empty($bbc_codes))
1058
	{
1059
		$temp_bbc = $bbc_codes;
1060
		$bbc_codes = array();
1061
	}
1062
1063
	// Ensure $modSettings['tld_regex'] contains a valid regex for the autolinker
1064
	if (!empty($modSettings['autoLinkUrls']))
1065
		set_tld_regex();
1066
1067
	// Allow mods access before entering the main parse_bbc loop
1068
	call_integration_hook('integrate_pre_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
1069
1070
	// Sift out the bbc for a performance improvement.
1071
	if (empty($bbc_codes) || $message === false || !empty($parse_tags))
1072
	{
1073
		if (!empty($modSettings['disabledBBC']))
1074
		{
1075
			$disabled = array();
1076
1077
			$temp = explode(',', strtolower($modSettings['disabledBBC']));
1078
1079
			foreach ($temp as $tag)
1080
				$disabled[trim($tag)] = true;
1081
1082
			if (in_array('color', $disabled))
1083
				$disabled = array_merge($disabled, array(
1084
					'black' => true,
1085
					'white' => true,
1086
					'red' => true,
1087
					'green' => true,
1088
					'blue' => true,
1089
					)
1090
				);
1091
		}
1092
1093
		// The YouTube bbc needs this for its origin parameter
1094
		$scripturl_parts = parse_url($scripturl);
1095
		$hosturl = $scripturl_parts['scheme'] . '://' . $scripturl_parts['host'];
1096
1097
		/* The following bbc are formatted as an array, with keys as follows:
1098
1099
			tag: the tag's name - should be lowercase!
1100
1101
			type: one of...
1102
				- (missing): [tag]parsed content[/tag]
1103
				- unparsed_equals: [tag=xyz]parsed content[/tag]
1104
				- parsed_equals: [tag=parsed data]parsed content[/tag]
1105
				- unparsed_content: [tag]unparsed content[/tag]
1106
				- closed: [tag], [tag/], [tag /]
1107
				- unparsed_commas: [tag=1,2,3]parsed content[/tag]
1108
				- unparsed_commas_content: [tag=1,2,3]unparsed content[/tag]
1109
				- unparsed_equals_content: [tag=...]unparsed content[/tag]
1110
1111
			parameters: an optional array of parameters, for the form
1112
			  [tag abc=123]content[/tag].  The array is an associative array
1113
			  where the keys are the parameter names, and the values are an
1114
			  array which may contain the following:
1115
				- match: a regular expression to validate and match the value.
1116
				- quoted: true if the value should be quoted.
1117
				- validate: callback to evaluate on the data, which is $data.
1118
				- value: a string in which to replace $1 with the data.
1119
					Either value or validate may be used, not both.
1120
				- optional: true if the parameter is optional.
1121
				- default: a default value for missing optional parameters.
1122
1123
			test: a regular expression to test immediately after the tag's
1124
			  '=', ' ' or ']'.  Typically, should have a \] at the end.
1125
			  Optional.
1126
1127
			content: only available for unparsed_content, closed,
1128
			  unparsed_commas_content, and unparsed_equals_content.
1129
			  $1 is replaced with the content of the tag.  Parameters
1130
			  are replaced in the form {param}.  For unparsed_commas_content,
1131
			  $2, $3, ..., $n are replaced.
1132
1133
			before: only when content is not used, to go before any
1134
			  content.  For unparsed_equals, $1 is replaced with the value.
1135
			  For unparsed_commas, $1, $2, ..., $n are replaced.
1136
1137
			after: similar to before in every way, except that it is used
1138
			  when the tag is closed.
1139
1140
			disabled_content: used in place of content when the tag is
1141
			  disabled.  For closed, default is '', otherwise it is '$1' if
1142
			  block_level is false, '<div>$1</div>' elsewise.
1143
1144
			disabled_before: used in place of before when disabled.  Defaults
1145
			  to '<div>' if block_level, '' if not.
1146
1147
			disabled_after: used in place of after when disabled.  Defaults
1148
			  to '</div>' if block_level, '' if not.
1149
1150
			block_level: set to true the tag is a "block level" tag, similar
1151
			  to HTML.  Block level tags cannot be nested inside tags that are
1152
			  not block level, and will not be implicitly closed as easily.
1153
			  One break following a block level tag may also be removed.
1154
1155
			trim: if set, and 'inside' whitespace after the begin tag will be
1156
			  removed.  If set to 'outside', whitespace after the end tag will
1157
			  meet the same fate.
1158
1159
			validate: except when type is missing or 'closed', a callback to
1160
			  validate the data as $data.  Depending on the tag's type, $data
1161
			  may be a string or an array of strings (corresponding to the
1162
			  replacement.)
1163
1164
			quoted: when type is 'unparsed_equals' or 'parsed_equals' only,
1165
			  may be not set, 'optional', or 'required' corresponding to if
1166
			  the content may be quoted.  This allows the parser to read
1167
			  [tag="abc]def[esdf]"] properly.
1168
1169
			require_parents: an array of tag names, or not set.  If set, the
1170
			  enclosing tag *must* be one of the listed tags, or parsing won't
1171
			  occur.
1172
1173
			require_children: similar to require_parents, if set children
1174
			  won't be parsed if they are not in the list.
1175
1176
			disallow_children: similar to, but very different from,
1177
			  require_children, if it is set the listed tags will not be
1178
			  parsed inside the tag.
1179
1180
			parsed_tags_allowed: an array restricting what BBC can be in the
1181
			  parsed_equals parameter, if desired.
1182
		*/
1183
1184
		$codes = array(
1185
			array(
1186
				'tag' => 'abbr',
1187
				'type' => 'unparsed_equals',
1188
				'before' => '<abbr title="$1">',
1189
				'after' => '</abbr>',
1190
				'quoted' => 'optional',
1191
				'disabled_after' => ' ($1)',
1192
			),
1193
			// Legacy (and just an alias for [abbr] even when enabled)
1194
			array(
1195
				'tag' => 'acronym',
1196
				'type' => 'unparsed_equals',
1197
				'before' => '<abbr title="$1">',
1198
				'after' => '</abbr>',
1199
				'quoted' => 'optional',
1200
				'disabled_after' => ' ($1)',
1201
			),
1202
			array(
1203
				'tag' => 'anchor',
1204
				'type' => 'unparsed_equals',
1205
				'test' => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]',
1206
				'before' => '<span id="post_$1">',
1207
				'after' => '</span>',
1208
			),
1209
			array(
1210
				'tag' => 'attach',
1211
				'type' => 'unparsed_content',
1212
				'parameters' => array(
1213
					'id' => array('match' => '(\d+)'),
1214
					'alt' => array('optional' => true),
1215
					'width' => array('optional' => true, 'match' => '(\d+)'),
1216
					'height' => array('optional' => true, 'match' => '(\d+)'),
1217
				),
1218
				'content' => '$1',
1219
				'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...
1220
				{
1221
					$returnContext = '';
1222
1223
					// BBC or the entire attachments feature is disabled
1224
					if (empty($modSettings['attachmentEnable']) || !empty($disabled['attach']))
1225
						return $data;
1226
1227
					// Save the attach ID.
1228
					$attachID = $params['{id}'];
1229
1230
					// Kinda need this.
1231
					require_once($sourcedir . '/Subs-Attachments.php');
1232
1233
					$currentAttachment = parseAttachBBC($attachID);
1234
1235
					// parseAttachBBC will return a string ($txt key) rather than dying with a fatal_error. Up to you to decide what to do.
1236
					if (is_string($currentAttachment))
1237
						return $data = !empty($txt[$currentAttachment]) ? $txt[$currentAttachment] : $currentAttachment;
1238
1239
					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...
1240
					{
1241
						$alt = ' alt="' . (!empty($params['{alt}']) ? $params['{alt}'] : $currentAttachment['name']) . '"';
1242
						$title = !empty($data) ? ' title="' . $smcFunc['htmlspecialchars']($data) . '"' : '';
1243
1244
						$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"' : '';
1245
						$height = !empty($params['{height}']) ? ' height="' . $params['{height}'] . '"' : '';
1246
1247
						if (empty($width) && empty($height))
1248
						{
1249
							$width = ' width="' . $currentAttachment['width'] . '"';
1250
							$height = ' height="' . $currentAttachment['height'] . '"';
1251
						}
1252
1253
						if ($currentAttachment['thumbnail']['has_thumb'] && empty($params['{width}']) && empty($params['{height}']))
1254
							$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>';
1255
						else
1256
							$returnContext .= '<img src="' . $currentAttachment['href'] . ';image"' . $alt . $title . $width . $height . ' class="bbc_img"/>';
1257
					}
1258
1259
					// No image. Show a link.
1260
					else
1261
						$returnContext .= '<a href="' . $currentAttachment['href'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . '</a>';
1262
1263
					// Gotta append what we just did.
1264
					$data = $returnContext;
1265
				},
1266
			),
1267
			array(
1268
				'tag' => 'b',
1269
				'before' => '<b>',
1270
				'after' => '</b>',
1271
			),
1272
			// Legacy (equivalent to [ltr] or [rtl])
1273
			array(
1274
				'tag' => 'bdo',
1275
				'type' => 'unparsed_equals',
1276
				'before' => '<bdo dir="$1">',
1277
				'after' => '</bdo>',
1278
				'test' => '(rtl|ltr)\]',
1279
				'block_level' => true,
1280
			),
1281
			// Legacy (alias of [color=black])
1282
			array(
1283
				'tag' => 'black',
1284
				'before' => '<span style="color: black;" class="bbc_color">',
1285
				'after' => '</span>',
1286
			),
1287
			// Legacy (alias of [color=blue])
1288
			array(
1289
				'tag' => 'blue',
1290
				'before' => '<span style="color: blue;" class="bbc_color">',
1291
				'after' => '</span>',
1292
			),
1293
			array(
1294
				'tag' => 'br',
1295
				'type' => 'closed',
1296
				'content' => '<br>',
1297
			),
1298
			array(
1299
				'tag' => 'center',
1300
				'before' => '<div class="centertext">',
1301
				'after' => '</div>',
1302
				'block_level' => true,
1303
			),
1304
			array(
1305
				'tag' => 'code',
1306
				'type' => 'unparsed_content',
1307
				'content' => '<div class="codeheader"><span class="code floatleft">' . $txt['code'] . '</span> <a class="codeoperation smf_select_text">' . $txt['code_select'] . '</a></div><code class="bbc_code">$1</code>',
1308
				// @todo Maybe this can be simplified?
1309
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1310
				{
1311
					if (!isset($disabled['code']))
1312
					{
1313
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
1314
1315
						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

1315
						for ($php_i = 0, $php_n = count(/** @scrutinizer ignore-type */ $php_parts); $php_i < $php_n; $php_i++)
Loading history...
1316
						{
1317
							// Do PHP code coloring?
1318
							if ($php_parts[$php_i] != '&lt;?php')
1319
								continue;
1320
1321
							$php_string = '';
1322
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1323
							{
1324
								$php_string .= $php_parts[$php_i];
1325
								$php_parts[$php_i++] = '';
1326
							}
1327
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1328
						}
1329
1330
						// Fix the PHP code stuff...
1331
						$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

1331
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', /** @scrutinizer ignore-type */ $php_parts));
Loading history...
1332
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1333
1334
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1335
						if (!empty($context['browser']['is_opera']))
1336
							$data .= '&nbsp;';
1337
					}
1338
				},
1339
				'block_level' => true,
1340
			),
1341
			array(
1342
				'tag' => 'code',
1343
				'type' => 'unparsed_equals_content',
1344
				'content' => '<div class="codeheader"><span class="code floatleft">' . $txt['code'] . '</span> ($2) <a class="codeoperation smf_select_text">' . $txt['code_select'] . '</a></div><code class="bbc_code">$1</code>',
1345
				// @todo Maybe this can be simplified?
1346
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1347
				{
1348
					if (!isset($disabled['code']))
1349
					{
1350
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1351
1352
						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

1352
						for ($php_i = 0, $php_n = count(/** @scrutinizer ignore-type */ $php_parts); $php_i < $php_n; $php_i++)
Loading history...
1353
						{
1354
							// Do PHP code coloring?
1355
							if ($php_parts[$php_i] != '&lt;?php')
1356
								continue;
1357
1358
							$php_string = '';
1359
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1360
							{
1361
								$php_string .= $php_parts[$php_i];
1362
								$php_parts[$php_i++] = '';
1363
							}
1364
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1365
						}
1366
1367
						// Fix the PHP code stuff...
1368
						$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

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

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

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

2571
					sort(/** @scrutinizer ignore-type */ $given_params, SORT_STRING);
Loading history...
2572
2573
					$match = preg_match('~^' . $params_regexes[$regex_key] . '$~i', implode(' ', $given_params), $matches) !== 0;
2574
2575
					if ($match)
2576
						break;
2577
				}
2578
2579
				// Didn't match our parameter list, try the next possible.
2580
				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...
2581
					continue;
2582
2583
				$params = array();
2584
				for ($i = 1, $n = count($matches); $i < $n; $i += 2)
2585
				{
2586
					$key = strtok(ltrim($matches[$i]), '=');
2587
					if ($key === false)
2588
						continue;
2589
					elseif (isset($possible['parameters'][$key]['value']))
2590
						$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
2591
					elseif (isset($possible['parameters'][$key]['validate']))
2592
						$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
2593
					else
2594
						$params['{' . $key . '}'] = $matches[$i + 1];
2595
2596
					// Just to make sure: replace any $ or { so they can't interpolate wrongly.
2597
					$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
2598
				}
2599
2600
				foreach ($possible['parameters'] as $p => $info)
2601
				{
2602
					if (!isset($params['{' . $p . '}']))
2603
					{
2604
						if (!isset($info['default']))
2605
							$params['{' . $p . '}'] = '';
2606
						elseif (isset($possible['parameters'][$p]['value']))
2607
							$params['{' . $p . '}'] = strtr($possible['parameters'][$p]['value'], array('$1' => $info['default']));
2608
						elseif (isset($possible['parameters'][$p]['validate']))
2609
							$params['{' . $p . '}'] = $possible['parameters'][$p]['validate']($info['default']);
2610
						else
2611
							$params['{' . $p . '}'] = $info['default'];
2612
					}
2613
				}
2614
2615
				$tag = $possible;
2616
2617
				// Put the parameters into the string.
2618
				if (isset($tag['before']))
2619
					$tag['before'] = strtr($tag['before'], $params);
2620
				if (isset($tag['after']))
2621
					$tag['after'] = strtr($tag['after'], $params);
2622
				if (isset($tag['content']))
2623
					$tag['content'] = strtr($tag['content'], $params);
2624
2625
				$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...
2626
			}
2627
			else
2628
			{
2629
				$tag = $possible;
2630
				$params = array();
2631
			}
2632
			break;
2633
		}
2634
2635
		// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
2636
		if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
2637
		{
2638
			if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
2639
				continue;
2640
2641
			$tag = $itemcodes[$message[$pos + 1]];
2642
2643
			// First let's set up the tree: it needs to be in a list, or after an li.
2644
			if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
2645
			{
2646
				$open_tags[] = array(
2647
					'tag' => 'list',
2648
					'after' => '</ul>',
2649
					'block_level' => true,
2650
					'require_children' => array('li'),
2651
					'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2652
				);
2653
				$code = '<ul class="bbc_list">';
2654
			}
2655
			// We're in a list item already: another itemcode?  Close it first.
2656
			elseif ($inside['tag'] == 'li')
2657
			{
2658
				array_pop($open_tags);
2659
				$code = '</li>';
2660
			}
2661
			else
2662
				$code = '';
2663
2664
			// Now we open a new tag.
2665
			$open_tags[] = array(
2666
				'tag' => 'li',
2667
				'after' => '</li>',
2668
				'trim' => 'outside',
2669
				'block_level' => true,
2670
				'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2671
			);
2672
2673
			// First, open the tag...
2674
			$code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
2675
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
2676
			$pos += strlen($code) - 1 + 2;
2677
2678
			// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
2679
			$pos2 = strpos($message, '<br>', $pos);
2680
			$pos3 = strpos($message, '[/', $pos);
2681
			if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
2682
			{
2683
				preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 4), $matches);
2684
				$message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
2685
2686
				$open_tags[count($open_tags) - 2]['after'] = '</ul>';
2687
			}
2688
			// Tell the [list] that it needs to close specially.
2689
			else
2690
			{
2691
				// Move the li over, because we're not sure what we'll hit.
2692
				$open_tags[count($open_tags) - 1]['after'] = '';
2693
				$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
2694
			}
2695
2696
			continue;
2697
		}
2698
2699
		// Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
2700
		if ($tag === null && $inside !== null && !empty($inside['require_children']))
2701
		{
2702
			array_pop($open_tags);
2703
2704
			$message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
2705
			$pos += strlen($inside['after']) - 1 + 2;
2706
		}
2707
2708
		// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
2709
		if ($tag === null)
2710
			continue;
2711
2712
		// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
2713
		if (isset($inside['disallow_children']))
2714
			$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
2715
2716
		// Is this tag disabled?
2717
		if (isset($disabled[$tag['tag']]))
2718
		{
2719
			if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
2720
			{
2721
				$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
2722
				$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
2723
				$tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
2724
			}
2725
			elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
2726
			{
2727
				$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
2728
				$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
2729
			}
2730
			else
2731
				$tag['content'] = $tag['disabled_content'];
2732
		}
2733
2734
		// we use this a lot
2735
		$tag_strlen = strlen($tag['tag']);
2736
2737
		// The only special case is 'html', which doesn't need to close things.
2738
		if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
2739
		{
2740
			$n = count($open_tags) - 1;
2741
			while (empty($open_tags[$n]['block_level']) && $n >= 0)
2742
				$n--;
2743
2744
			// Close all the non block level tags so this tag isn't surrounded by them.
2745
			for ($i = count($open_tags) - 1; $i > $n; $i--)
2746
			{
2747
				$message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
2748
				$ot_strlen = strlen($open_tags[$i]['after']);
2749
				$pos += $ot_strlen + 2;
2750
				$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...
2751
2752
				// Trim or eat trailing stuff... see comment at the end of the big loop.
2753
				$whitespace_regex = '';
2754
				if (!empty($tag['block_level']))
2755
					$whitespace_regex .= '(&nbsp;|\s)*(<br>)?';
2756
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
2757
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2758
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
2759
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2760
2761
				array_pop($open_tags);
2762
			}
2763
		}
2764
2765
		// Can't read past the end of the message
2766
		$pos1 = min(strlen($message), $pos1);
2767
2768
		// No type means 'parsed_content'.
2769
		if (!isset($tag['type']))
2770
		{
2771
			$open_tags[] = $tag;
2772
			$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
2773
			$pos += strlen($tag['before']) - 1 + 2;
2774
		}
2775
		// Don't parse the content, just skip it.
2776
		elseif ($tag['type'] == 'unparsed_content')
2777
		{
2778
			$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
2779
			if ($pos2 === false)
2780
				continue;
2781
2782
			$data = substr($message, $pos1, $pos2 - $pos1);
2783
2784
			if (!empty($tag['block_level']) && substr($data, 0, 4) == '<br>')
2785
				$data = substr($data, 4);
2786
2787
			if (isset($tag['validate']))
2788
				$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...
2789
2790
			$code = strtr($tag['content'], array('$1' => $data));
2791
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
2792
2793
			$pos += strlen($code) - 1 + 2;
2794
			$last_pos = $pos + 1;
2795
		}
2796
		// Don't parse the content, just skip it.
2797
		elseif ($tag['type'] == 'unparsed_equals_content')
2798
		{
2799
			// The value may be quoted for some tags - check.
2800
			if (isset($tag['quoted']))
2801
			{
2802
				$quoted = substr($message, $pos1, 6) == '&quot;';
2803
				if ($tag['quoted'] != 'optional' && !$quoted)
2804
					continue;
2805
2806
				if ($quoted)
2807
					$pos1 += 6;
2808
			}
2809
			else
2810
				$quoted = false;
2811
2812
			$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...
2813
			if ($pos2 === false)
2814
				continue;
2815
2816
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
2817
			if ($pos3 === false)
2818
				continue;
2819
2820
			$data = array(
2821
				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...
2822
				substr($message, $pos1, $pos2 - $pos1)
2823
			);
2824
2825
			if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '<br>')
2826
				$data[0] = substr($data[0], 4);
2827
2828
			// Validation for my parking, please!
2829
			if (isset($tag['validate']))
2830
				$tag['validate']($tag, $data, $disabled, $params);
2831
2832
			$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
2833
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
2834
			$pos += strlen($code) - 1 + 2;
2835
		}
2836
		// A closed tag, with no content or value.
2837
		elseif ($tag['type'] == 'closed')
2838
		{
2839
			$pos2 = strpos($message, ']', $pos);
2840
			$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
2841
			$pos += strlen($tag['content']) - 1 + 2;
2842
		}
2843
		// This one is sorta ugly... :/.  Unfortunately, it's needed for flash.
2844
		elseif ($tag['type'] == 'unparsed_commas_content')
2845
		{
2846
			$pos2 = strpos($message, ']', $pos1);
2847
			if ($pos2 === false)
2848
				continue;
2849
2850
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
2851
			if ($pos3 === false)
2852
				continue;
2853
2854
			// We want $1 to be the content, and the rest to be csv.
2855
			$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
2856
			$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
2857
2858
			if (isset($tag['validate']))
2859
				$tag['validate']($tag, $data, $disabled, $params);
2860
2861
			$code = $tag['content'];
2862
			foreach ($data as $k => $d)
2863
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
2864
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
2865
			$pos += strlen($code) - 1 + 2;
2866
		}
2867
		// This has parsed content, and a csv value which is unparsed.
2868
		elseif ($tag['type'] == 'unparsed_commas')
2869
		{
2870
			$pos2 = strpos($message, ']', $pos1);
2871
			if ($pos2 === false)
2872
				continue;
2873
2874
			$data = explode(',', substr($message, $pos1, $pos2 - $pos1));
2875
2876
			if (isset($tag['validate']))
2877
				$tag['validate']($tag, $data, $disabled, $params);
2878
2879
			// Fix after, for disabled code mainly.
2880
			foreach ($data as $k => $d)
2881
				$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
2882
2883
			$open_tags[] = $tag;
2884
2885
			// Replace them out, $1, $2, $3, $4, etc.
2886
			$code = $tag['before'];
2887
			foreach ($data as $k => $d)
2888
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
2889
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1);
2890
			$pos += strlen($code) - 1 + 2;
2891
		}
2892
		// A tag set to a value, parsed or not.
2893
		elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
2894
		{
2895
			// The value may be quoted for some tags - check.
2896
			if (isset($tag['quoted']))
2897
			{
2898
				$quoted = substr($message, $pos1, 6) == '&quot;';
2899
				if ($tag['quoted'] != 'optional' && !$quoted)
2900
					continue;
2901
2902
				if ($quoted)
2903
					$pos1 += 6;
2904
			}
2905
			else
2906
				$quoted = false;
2907
2908
			$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...
2909
			if ($pos2 === false)
2910
				continue;
2911
2912
			$data = substr($message, $pos1, $pos2 - $pos1);
2913
2914
			// Validation for my parking, please!
2915
			if (isset($tag['validate']))
2916
				$tag['validate']($tag, $data, $disabled, $params);
2917
2918
			// For parsed content, we must recurse to avoid security problems.
2919
			if ($tag['type'] != 'unparsed_equals')
2920
				$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
2921
2922
			$tag['after'] = strtr($tag['after'], array('$1' => $data));
2923
2924
			$open_tags[] = $tag;
2925
2926
			$code = strtr($tag['before'], array('$1' => $data));
2927
			$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...
2928
			$pos += strlen($code) - 1 + 2;
2929
		}
2930
2931
		// If this is block level, eat any breaks after it.
2932
		if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '<br>')
2933
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 5);
2934
2935
		// Are we trimming outside this tag?
2936
		if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
2937
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
2938
	}
2939
2940
	// Close any remaining tags.
2941
	while ($tag = array_pop($open_tags))
2942
		$message .= "\n" . $tag['after'] . "\n";
2943
2944
	// Parse the smileys within the parts where it can be done safely.
2945
	if ($smileys === true)
2946
	{
2947
		$message_parts = explode("\n", $message);
2948
		for ($i = 0, $n = count($message_parts); $i < $n; $i += 2)
2949
			parsesmileys($message_parts[$i]);
2950
2951
		$message = implode('', $message_parts);
2952
	}
2953
2954
	// No smileys, just get rid of the markers.
2955
	else
2956
		$message = strtr($message, array("\n" => ''));
2957
2958
	if ($message !== '' && $message[0] === ' ')
2959
		$message = '&nbsp;' . substr($message, 1);
2960
2961
	// Cleanup whitespace.
2962
	$message = strtr($message, array('  ' => ' &nbsp;', "\r" => '', "\n" => '<br>', '<br> ' => '<br>&nbsp;', '&#13;' => "\n"));
2963
2964
	// Allow mods access to what parse_bbc created
2965
	call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
2966
2967
	// Cache the output if it took some time...
2968
	if (isset($cache_key, $cache_t) && microtime(true) - $cache_t > 0.05)
2969
		cache_put_data($cache_key, $message, 240);
2970
2971
	// If this was a force parse revert if needed.
2972
	if (!empty($parse_tags))
2973
	{
2974
		if (empty($temp_bbc))
2975
			$bbc_codes = array();
2976
		else
2977
		{
2978
			$bbc_codes = $temp_bbc;
2979
			unset($temp_bbc);
2980
		}
2981
2982
		if (empty($real_alltags_regex))
2983
			$alltags_regex = '';
2984
		else
2985
		{
2986
			$alltags_regex = $real_alltags_regex;
2987
			unset($real_alltags_regex);
2988
		}
2989
	}
2990
2991
	return $message;
2992
}
2993
2994
/**
2995
 * Parse smileys in the passed message.
2996
 *
2997
 * The smiley parsing function which makes pretty faces appear :).
2998
 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
2999
 * These are specifically not parsed in code tags [url=mailto:[email protected]]
3000
 * Caches the smileys from the database or array in memory.
3001
 * Doesn't return anything, but rather modifies message directly.
3002
 *
3003
 * @param string &$message The message to parse smileys in
3004
 */
3005
function parsesmileys(&$message)
3006
{
3007
	global $modSettings, $txt, $user_info, $context, $smcFunc;
3008
	static $smileyPregSearch = null, $smileyPregReplacements = array();
3009
3010
	// No smiley set at all?!
3011
	if ($user_info['smiley_set'] == 'none' || trim($message) == '')
3012
		return;
3013
3014
	// Maybe a mod wants to implement an alternative method (e.g. emojis instead of images)
3015
	call_integration_hook('integrate_smileys', array(&$smileyPregSearch, &$smileyPregReplacements));
3016
3017
	// If smileyPregSearch hasn't been set, do it now.
3018
	if (empty($smileyPregSearch))
3019
	{
3020
		// Cache for longer when customized smiley codes aren't enabled
3021
		$cache_time = empty($modSettings['smiley_enable']) ? 7200 : 480;
3022
3023
		// Load the smileys in reverse order by length so they don't get parsed incorrectly.
3024
		if (($temp = cache_get_data('parsing_smileys_' . $user_info['smiley_set'], $cache_time)) == null)
3025
		{
3026
			$result = $smcFunc['db_query']('', '
3027
				SELECT s.code, f.filename, s.description
3028
				FROM {db_prefix}smileys AS s
3029
					JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
3030
				WHERE f.smiley_set = {string:smiley_set}' . (empty($modSettings['smiley_enable']) ? '
3031
					AND s.code IN ({array_string:default_codes})' : '') . '
3032
				ORDER BY LENGTH(s.code) DESC',
3033
				array(
3034
					'default_codes' => array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'),
3035
					'smiley_set' => $user_info['smiley_set'],
3036
				)
3037
			);
3038
			$smileysfrom = array();
3039
			$smileysto = array();
3040
			$smileysdescs = array();
3041
			while ($row = $smcFunc['db_fetch_assoc']($result))
3042
			{
3043
				$smileysfrom[] = $row['code'];
3044
				$smileysto[] = $smcFunc['htmlspecialchars']($row['filename']);
3045
				$smileysdescs[] = !empty($txt['icon_' . strtolower($row['description'])]) ? $txt['icon_' . strtolower($row['description'])] : $row['description'];
3046
			}
3047
			$smcFunc['db_free_result']($result);
3048
3049
			cache_put_data('parsing_smileys_' . $user_info['smiley_set'], array($smileysfrom, $smileysto, $smileysdescs), $cache_time);
3050
		}
3051
		else
3052
			list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
3053
3054
		// The non-breaking-space is a complex thing...
3055
		$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
3056
3057
		// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:[email protected]] doesn't parse the :D smiley)
3058
		$smileyPregReplacements = array();
3059
		$searchParts = array();
3060
		$smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
3061
3062
		for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
3063
		{
3064
			$specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES);
3065
			$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">';
3066
3067
			$smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
3068
3069
			$searchParts[] = $smileysfrom[$i];
3070
			if ($smileysfrom[$i] != $specialChars)
3071
			{
3072
				$smileyPregReplacements[$specialChars] = $smileyCode;
3073
				$searchParts[] = $specialChars;
3074
3075
				// Some 2.0 hex htmlchars are in there as 3 digits; allow for finding leading 0 or not
3076
				$specialChars2 = preg_replace('/&#(\d{2});/', '&#0$1;', $specialChars);
3077
				if ($specialChars2 != $specialChars)
3078
				{
3079
					$smileyPregReplacements[$specialChars2] = $smileyCode;
3080
					$searchParts[] = $specialChars2;
3081
				}
3082
			}
3083
		}
3084
3085
		$smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?<![a-zA-Z0-9])\(|^)(' . build_regex($searchParts, '~') . ')(?=[^[:alpha:]0-9]|$)~' . ($context['utf8'] ? 'u' : '');
3086
	}
3087
3088
	// Replace away!
3089
	$message = preg_replace_callback($smileyPregSearch, function($matches) use ($smileyPregReplacements)
3090
		{
3091
			return $smileyPregReplacements[$matches[1]];
3092
		}, $message);
3093
}
3094
3095
/**
3096
 * Highlight any code.
3097
 *
3098
 * Uses PHP's highlight_string() to highlight PHP syntax
3099
 * does special handling to keep the tabs in the code available.
3100
 * used to parse PHP code from inside [code] and [php] tags.
3101
 *
3102
 * @param string $code The code
3103
 * @return string The code with highlighted HTML.
3104
 */
3105
function highlight_php_code($code)
3106
{
3107
	// Remove special characters.
3108
	$code = un_htmlspecialchars(strtr($code, array('<br />' => "\n", '<br>' => "\n", "\t" => 'SMF_TAB();', '&#91;' => '[')));
3109
3110
	$oldlevel = error_reporting(0);
3111
3112
	$buffer = str_replace(array("\n", "\r"), '', @highlight_string($code, true));
3113
3114
	error_reporting($oldlevel);
3115
3116
	// Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
3117
	$buffer = preg_replace('~SMF_TAB(?:</(?:font|span)><(?:font color|span style)="[^"]*?">)?\\(\\);~', '<pre style="display: inline;">' . "\t" . '</pre>', $buffer);
3118
3119
	return strtr($buffer, array('\'' => '&#039;', '<code>' => '', '</code>' => ''));
3120
}
3121
3122
/**
3123
 * Gets the appropriate URL to use for images (or whatever) when using SSL
3124
 *
3125
 * The returned URL may or may not be a proxied URL, depending on the situation.
3126
 * Mods can implement alternative proxies using the 'integrate_proxy' hook.
3127
 *
3128
 * @param string $url The original URL of the requested resource
3129
 * @return string The URL to use
3130
 */
3131
function get_proxied_url($url)
3132
{
3133
	global $boardurl, $image_proxy_enabled, $image_proxy_secret;
3134
3135
	// Only use the proxy if enabled and necessary
3136
	if (empty($image_proxy_enabled) || parse_url($url, PHP_URL_SCHEME) === 'https')
3137
		return $url;
3138
3139
	// We don't need to proxy our own resources
3140
	if (strpos(strtr($url, array('http://' => 'https://')), strtr($boardurl, array('http://' => 'https://'))) === 0)
3141
		return strtr($url, array('http://' => 'https://'));
3142
3143
	// By default, use SMF's own image proxy script
3144
	$proxied_url = strtr($boardurl, array('http://' => 'https://')) . '/proxy.php?request=' . urlencode($url) . '&hash=' . md5($url . $image_proxy_secret);
3145
3146
	// Allow mods to easily implement an alternative proxy
3147
	// MOD AUTHORS: To add settings UI for your proxy, use the integrate_general_settings hook.
3148
	call_integration_hook('integrate_proxy', array($url, &$proxied_url));
3149
3150
	return $proxied_url;
3151
}
3152
3153
/**
3154
 * Make sure the browser doesn't come back and repost the form data.
3155
 * Should be used whenever anything is posted.
3156
 *
3157
 * @param string $setLocation The URL to redirect them to
3158
 * @param bool $refresh Whether to use a meta refresh instead
3159
 * @param bool $permanent Whether to send a 301 Moved Permanently instead of a 302 Moved Temporarily
3160
 */
3161
function redirectexit($setLocation = '', $refresh = false, $permanent = false)
3162
{
3163
	global $scripturl, $context, $modSettings, $db_show_debug, $db_cache;
3164
3165
	// In case we have mail to send, better do that - as obExit doesn't always quite make it...
3166
	if (!empty($context['flush_mail']))
3167
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
3168
		AddMailQueue(true);
3169
3170
	$add = preg_match('~^(ftp|http)[s]?://~', $setLocation) == 0 && substr($setLocation, 0, 6) != 'about:';
3171
3172
	if ($add)
3173
		$setLocation = $scripturl . ($setLocation != '' ? '?' . $setLocation : '');
3174
3175
	// Put the session ID in.
3176
	if (defined('SID') && SID != '')
3177
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', $scripturl . '?' . SID . ';', $setLocation);
3178
	// Keep that debug in their for template debugging!
3179
	elseif (isset($_GET['debug']))
3180
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);
3181
3182
	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'])))
3183
	{
3184
		if (defined('SID') && SID != '')
3185
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '~') . '\?(?:' . SID . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~',
3186
				function($m) use ($scripturl)
3187
				{
3188
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . SID . (isset($m[2]) ? "$m[2]" : "");
3189
				}, $setLocation);
3190
		else
3191
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '~') . '\?((?:board|topic)=[^#"]+?)(#[^"]*?)?$~',
3192
				function($m) use ($scripturl)
3193
				{
3194
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html' . (isset($m[2]) ? "$m[2]" : "");
3195
				}, $setLocation);
3196
	}
3197
3198
	// Maybe integrations want to change where we are heading?
3199
	call_integration_hook('integrate_redirect', array(&$setLocation, &$refresh, &$permanent));
3200
3201
	// Set the header.
3202
	header('location: ' . str_replace(' ', '%20', $setLocation), true, $permanent ? 301 : 302);
3203
3204
	// Debugging.
3205
	if (isset($db_show_debug) && $db_show_debug === true)
3206
		$_SESSION['debug_redirect'] = $db_cache;
3207
3208
	obExit(false);
3209
}
3210
3211
/**
3212
 * Ends execution.  Takes care of template loading and remembering the previous URL.
3213
 *
3214
 * @param bool $header Whether to do the header
3215
 * @param bool $do_footer Whether to do the footer
3216
 * @param bool $from_index Whether we're coming from the board index
3217
 * @param bool $from_fatal_error Whether we're coming from a fatal error
3218
 */
3219
function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false)
3220
{
3221
	global $context, $settings, $modSettings, $txt, $smcFunc;
3222
	static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
3223
3224
	// Attempt to prevent a recursive loop.
3225
	++$level;
3226
	if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
3227
		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...
3228
	if ($from_fatal_error)
3229
		$has_fatal_error = true;
3230
3231
	// Clear out the stat cache.
3232
	trackStats();
3233
3234
	// If we have mail to send, send it.
3235
	if (!empty($context['flush_mail']))
3236
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
3237
		AddMailQueue(true);
3238
3239
	$do_header = $header === null ? !$header_done : $header;
3240
	if ($do_footer === null)
3241
		$do_footer = $do_header;
3242
3243
	// Has the template/header been done yet?
3244
	if ($do_header)
3245
	{
3246
		// Was the page title set last minute? Also update the HTML safe one.
3247
		if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
3248
			$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3249
3250
		// Start up the session URL fixer.
3251
		ob_start('ob_sessrewrite');
3252
3253
		if (!empty($settings['output_buffers']) && is_string($settings['output_buffers']))
3254
			$buffers = explode(',', $settings['output_buffers']);
3255
		elseif (!empty($settings['output_buffers']))
3256
			$buffers = $settings['output_buffers'];
3257
		else
3258
			$buffers = array();
3259
3260
		if (isset($modSettings['integrate_buffer']))
3261
			$buffers = array_merge(explode(',', $modSettings['integrate_buffer']), $buffers);
3262
3263
		if (!empty($buffers))
3264
			foreach ($buffers as $function)
3265
			{
3266
				$call = call_helper($function, true);
3267
3268
				// Is it valid?
3269
				if (!empty($call))
3270
					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

3270
					ob_start(/** @scrutinizer ignore-type */ $call);
Loading history...
3271
			}
3272
3273
		// Display the screen in the logical order.
3274
		template_header();
3275
		$header_done = true;
3276
	}
3277
	if ($do_footer)
3278
	{
3279
		loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main');
3280
3281
		// Anything special to put out?
3282
		if (!empty($context['insert_after_template']) && !isset($_REQUEST['xml']))
3283
			echo $context['insert_after_template'];
3284
3285
		// Just so we don't get caught in an endless loop of errors from the footer...
3286
		if (!$footer_done)
3287
		{
3288
			$footer_done = true;
3289
			template_footer();
3290
3291
			// (since this is just debugging... it's okay that it's after </html>.)
3292
			if (!isset($_REQUEST['xml']))
3293
				displayDebug();
3294
		}
3295
	}
3296
3297
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
3298
	if (strpos($_SERVER['REQUEST_URL'], 'action=dlattach') === false && strpos($_SERVER['REQUEST_URL'], 'action=viewsmfile') === false)
3299
		$_SESSION['old_url'] = $_SERVER['REQUEST_URL'];
3300
3301
	// For session check verification.... don't switch browsers...
3302
	$_SESSION['USER_AGENT'] = empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT'];
3303
3304
	// Hand off the output to the portal, etc. we're integrated with.
3305
	call_integration_hook('integrate_exit', array($do_footer));
3306
3307
	// Don't exit if we're coming from index.php; that will pass through normally.
3308
	if (!$from_index)
3309
		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...
3310
}
3311
3312
/**
3313
 * Get the size of a specified image with better error handling.
3314
 *
3315
 * @todo see if it's better in Subs-Graphics, but one step at the time.
3316
 * Uses getimagesize() to determine the size of a file.
3317
 * Attempts to connect to the server first so it won't time out.
3318
 *
3319
 * @param string $url The URL of the image
3320
 * @return array|false The image size as array (width, height), or false on failure
3321
 */
3322
function url_image_size($url)
3323
{
3324
	global $sourcedir;
3325
3326
	// Make sure it is a proper URL.
3327
	$url = str_replace(' ', '%20', $url);
3328
3329
	// Can we pull this from the cache... please please?
3330
	if (($temp = cache_get_data('url_image_size-' . md5($url), 240)) !== null)
0 ignored issues
show
introduced by
The condition $temp = cache_get_data('...d5($url), 240) !== null is always true.
Loading history...
3331
		return $temp;
3332
	$t = microtime(true);
3333
3334
	// Get the host to pester...
3335
	preg_match('~^\w+://(.+?)/(.*)$~', $url, $match);
3336
3337
	// Can't figure it out, just try the image size.
3338
	if ($url == '' || $url == 'http://' || $url == 'https://')
3339
	{
3340
		return false;
3341
	}
3342
	elseif (!isset($match[1]))
3343
	{
3344
		$size = @getimagesize($url);
3345
	}
3346
	else
3347
	{
3348
		// Try to connect to the server... give it half a second.
3349
		$temp = 0;
3350
		$fp = @fsockopen($match[1], 80, $temp, $temp, 0.5);
3351
3352
		// Successful?  Continue...
3353
		if ($fp != false)
3354
		{
3355
			// Send the HEAD request (since we don't have to worry about chunked, HTTP/1.1 is fine here.)
3356
			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");
3357
3358
			// Read in the HTTP/1.1 or whatever.
3359
			$test = substr(fgets($fp, 11), -1);
3360
			fclose($fp);
3361
3362
			// See if it returned a 404/403 or something.
3363
			if ($test < 4)
3364
			{
3365
				$size = @getimagesize($url);
3366
3367
				// This probably means allow_url_fopen is off, let's try GD.
3368
				if ($size === false && function_exists('imagecreatefromstring'))
3369
				{
3370
					// It's going to hate us for doing this, but another request...
3371
					$image = @imagecreatefromstring(fetch_web_data($url));
3372
					if ($image !== false)
3373
					{
3374
						$size = array(imagesx($image), imagesy($image));
3375
						imagedestroy($image);
3376
					}
3377
				}
3378
			}
3379
		}
3380
	}
3381
3382
	// If we didn't get it, we failed.
3383
	if (!isset($size))
3384
		$size = false;
3385
3386
	// If this took a long time, we may never have to do it again, but then again we might...
3387
	if (microtime(true) - $t > 0.8)
3388
		cache_put_data('url_image_size-' . md5($url), $size, 240);
3389
3390
	// Didn't work.
3391
	return $size;
3392
}
3393
3394
/**
3395
 * Sets up the basic theme context stuff.
3396
 *
3397
 * @param bool $forceload Whether to load the theme even if it's already loaded
3398
 */
3399
function setupThemeContext($forceload = false)
3400
{
3401
	global $modSettings, $user_info, $scripturl, $context, $settings, $options, $txt, $maintenance;
3402
	global $smcFunc;
3403
	static $loaded = false;
3404
3405
	// Under SSI this function can be called more then once.  That can cause some problems.
3406
	//   So only run the function once unless we are forced to run it again.
3407
	if ($loaded && !$forceload)
3408
		return;
3409
3410
	$loaded = true;
3411
3412
	$context['in_maintenance'] = !empty($maintenance);
3413
	$context['current_time'] = timeformat(time(), false);
3414
	$context['current_action'] = isset($_GET['action']) ? $smcFunc['htmlspecialchars']($_GET['action']) : '';
3415
3416
	// Get some news...
3417
	$context['news_lines'] = array_filter(explode("\n", str_replace("\r", '', trim(addslashes($modSettings['news'])))));
3418
	for ($i = 0, $n = count($context['news_lines']); $i < $n; $i++)
3419
	{
3420
		if (trim($context['news_lines'][$i]) == '')
3421
			continue;
3422
3423
		// Clean it up for presentation ;).
3424
		$context['news_lines'][$i] = parse_bbc(stripslashes(trim($context['news_lines'][$i])), true, 'news' . $i);
3425
	}
3426
	if (!empty($context['news_lines']))
3427
		$context['random_news_line'] = $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)];
3428
3429
	if (!$user_info['is_guest'])
3430
	{
3431
		$context['user']['messages'] = &$user_info['messages'];
3432
		$context['user']['unread_messages'] = &$user_info['unread_messages'];
3433
		$context['user']['alerts'] = &$user_info['alerts'];
3434
3435
		// Personal message popup...
3436
		if ($user_info['unread_messages'] > (isset($_SESSION['unread_messages']) ? $_SESSION['unread_messages'] : 0))
3437
			$context['user']['popup_messages'] = true;
3438
		else
3439
			$context['user']['popup_messages'] = false;
3440
		$_SESSION['unread_messages'] = $user_info['unread_messages'];
3441
3442
		if (allowedTo('moderate_forum'))
3443
			$context['unapproved_members'] = !empty($modSettings['unapprovedMembers']) ? $modSettings['unapprovedMembers'] : 0;
3444
3445
		$context['user']['avatar'] = array();
3446
3447
		// Check for gravatar first since we might be forcing them...
3448
		if (($modSettings['gravatarEnabled'] && substr($user_info['avatar']['url'], 0, 11) == 'gravatar://') || !empty($modSettings['gravatarOverride']))
3449
		{
3450
			if (!empty($modSettings['gravatarAllowExtraEmail']) && stristr($user_info['avatar']['url'], 'gravatar://') && strlen($user_info['avatar']['url']) > 11)
3451
				$context['user']['avatar']['href'] = get_gravatar_url($smcFunc['substr']($user_info['avatar']['url'], 11));
3452
			else
3453
				$context['user']['avatar']['href'] = get_gravatar_url($user_info['email']);
3454
		}
3455
		// Uploaded?
3456
		elseif ($user_info['avatar']['url'] == '' && !empty($user_info['avatar']['id_attach']))
3457
			$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';
3458
		// Full URL?
3459
		elseif (strpos($user_info['avatar']['url'], 'http://') === 0 || strpos($user_info['avatar']['url'], 'https://') === 0)
3460
			$context['user']['avatar']['href'] = $user_info['avatar']['url'];
3461
		// Otherwise we assume it's server stored.
3462
		elseif ($user_info['avatar']['url'] != '')
3463
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/' . $smcFunc['htmlspecialchars']($user_info['avatar']['url']);
3464
		// No avatar at all? Fine, we have a big fat default avatar ;)
3465
		else
3466
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/default.png';
3467
3468
		if (!empty($context['user']['avatar']))
3469
			$context['user']['avatar']['image'] = '<img src="' . $context['user']['avatar']['href'] . '" alt="" class="avatar">';
3470
3471
		// Figure out how long they've been logged in.
3472
		$context['user']['total_time_logged_in'] = array(
3473
			'days' => floor($user_info['total_time_logged_in'] / 86400),
3474
			'hours' => floor(($user_info['total_time_logged_in'] % 86400) / 3600),
3475
			'minutes' => floor(($user_info['total_time_logged_in'] % 3600) / 60)
3476
		);
3477
	}
3478
	else
3479
	{
3480
		$context['user']['messages'] = 0;
3481
		$context['user']['unread_messages'] = 0;
3482
		$context['user']['avatar'] = array();
3483
		$context['user']['total_time_logged_in'] = array('days' => 0, 'hours' => 0, 'minutes' => 0);
3484
		$context['user']['popup_messages'] = false;
3485
3486
		if (!empty($modSettings['registration_method']) && $modSettings['registration_method'] == 1)
3487
			$txt['welcome_guest'] .= $txt['welcome_guest_activate'];
3488
3489
		// If we've upgraded recently, go easy on the passwords.
3490
		if (!empty($modSettings['disableHashTime']) && ($modSettings['disableHashTime'] == 1 || time() < $modSettings['disableHashTime']))
3491
			$context['disable_login_hashing'] = true;
3492
	}
3493
3494
	// Setup the main menu items.
3495
	setupMenuContext();
3496
3497
	// This is here because old index templates might still use it.
3498
	$context['show_news'] = !empty($settings['enable_news']);
3499
3500
	// This is done to allow theme authors to customize it as they want.
3501
	$context['show_pm_popup'] = $context['user']['popup_messages'] && !empty($options['popup_messages']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != 'pm');
3502
3503
	// 2.1+: Add the PM popup here instead. Theme authors can still override it simply by editing/removing the 'fPmPopup' in the array.
3504
	if ($context['show_pm_popup'])
3505
		addInlineJavaScript('
3506
		jQuery(document).ready(function($) {
3507
			new smc_Popup({
3508
				heading: ' . JavaScriptEscape($txt['show_personal_messages_heading']) . ',
3509
				content: ' . JavaScriptEscape(sprintf($txt['show_personal_messages'], $context['user']['unread_messages'], $scripturl . '?action=pm')) . ',
3510
				icon_class: \'main_icons mail_new\'
3511
			});
3512
		});');
3513
3514
	// Add a generic "Are you sure?" confirmation message.
3515
	addInlineJavaScript('
3516
	var smf_you_sure =' . JavaScriptEscape($txt['quickmod_confirm']) . ';');
3517
3518
	// Now add the capping code for avatars.
3519
	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')
3520
		addInlineCss('
3521
	img.avatar { max-width: ' . $modSettings['avatar_max_width_external'] . 'px; max-height: ' . $modSettings['avatar_max_height_external'] . 'px; }');
3522
3523
	// Add max image limits
3524
	if (!empty($modSettings['max_image_width']))
3525
		addInlineCss('
3526
	.postarea .bbc_img { max-width: ' . $modSettings['max_image_width'] . 'px; }');
3527
3528
	if (!empty($modSettings['max_image_height']))
3529
		addInlineCss('
3530
	.postarea .bbc_img { max-height: ' . $modSettings['max_image_height'] . 'px; }');
3531
3532
	// This looks weird, but it's because BoardIndex.php references the variable.
3533
	$context['common_stats']['latest_member'] = array(
3534
		'id' => $modSettings['latestMember'],
3535
		'name' => $modSettings['latestRealName'],
3536
		'href' => $scripturl . '?action=profile;u=' . $modSettings['latestMember'],
3537
		'link' => '<a href="' . $scripturl . '?action=profile;u=' . $modSettings['latestMember'] . '">' . $modSettings['latestRealName'] . '</a>',
3538
	);
3539
	$context['common_stats'] = array(
3540
		'total_posts' => comma_format($modSettings['totalMessages']),
3541
		'total_topics' => comma_format($modSettings['totalTopics']),
3542
		'total_members' => comma_format($modSettings['totalMembers']),
3543
		'latest_member' => $context['common_stats']['latest_member'],
3544
	);
3545
	$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']);
3546
3547
	if (empty($settings['theme_version']))
3548
		addJavaScriptVar('smf_scripturl', $scripturl);
3549
3550
	if (!isset($context['page_title']))
3551
		$context['page_title'] = '';
3552
3553
	// Set some specific vars.
3554
	$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3555
	$context['meta_keywords'] = !empty($modSettings['meta_keywords']) ? $smcFunc['htmlspecialchars']($modSettings['meta_keywords']) : '';
3556
3557
	// Content related meta tags, including Open Graph
3558
	$context['meta_tags'][] = array('property' => 'og:site_name', 'content' => $context['forum_name']);
3559
	$context['meta_tags'][] = array('property' => 'og:title', 'content' => $context['page_title_html_safe']);
3560
3561
	if (!empty($context['meta_keywords']))
3562
		$context['meta_tags'][] = array('name' => 'keywords', 'content' => $context['meta_keywords']);
3563
3564
	if (!empty($context['canonical_url']))
3565
		$context['meta_tags'][] = array('property' => 'og:url', 'content' => $context['canonical_url']);
3566
3567
	if (!empty($settings['og_image']))
3568
		$context['meta_tags'][] = array('property' => 'og:image', 'content' => $settings['og_image']);
3569
3570
	if (!empty($context['meta_description']))
3571
	{
3572
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['meta_description']);
3573
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['meta_description']);
3574
	}
3575
	else
3576
	{
3577
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['page_title_html_safe']);
3578
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['page_title_html_safe']);
3579
	}
3580
3581
	call_integration_hook('integrate_theme_context');
3582
}
3583
3584
/**
3585
 * Helper function to set the system memory to a needed value
3586
 * - If the needed memory is greater than current, will attempt to get more
3587
 * - if in_use is set to true, will also try to take the current memory usage in to account
3588
 *
3589
 * @param string $needed The amount of memory to request, if needed, like 256M
3590
 * @param bool $in_use Set to true to account for current memory usage of the script
3591
 * @return boolean True if we have at least the needed memory
3592
 */
3593
function setMemoryLimit($needed, $in_use = false)
3594
{
3595
	// everything in bytes
3596
	$memory_current = memoryReturnBytes(ini_get('memory_limit'));
3597
	$memory_needed = memoryReturnBytes($needed);
3598
3599
	// should we account for how much is currently being used?
3600
	if ($in_use)
3601
		$memory_needed += function_exists('memory_get_usage') ? memory_get_usage() : (2 * 1048576);
3602
3603
	// if more is needed, request it
3604
	if ($memory_current < $memory_needed)
3605
	{
3606
		@ini_set('memory_limit', ceil($memory_needed / 1048576) . 'M');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for ini_set(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

3606
		/** @scrutinizer ignore-unhandled */ @ini_set('memory_limit', ceil($memory_needed / 1048576) . 'M');

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

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

4834
			$results[$function] = call_user_func_array(/** @scrutinizer ignore-type */ $call, $parameters);
Loading history...
4835
		// This failed, but we want to do so silently.
4836
		elseif (!empty($function) && !empty($context['ignore_hook_errors']))
4837
			return $results;
4838
		// Whatever it was suppose to call, it failed :(
4839
		elseif (!empty($function))
4840
		{
4841
			loadLanguage('Errors');
4842
4843
			// Get a full path to show on error.
4844
			if (strpos($function, '|') !== false)
4845
			{
4846
				list ($file, $string) = explode('|', $function);
4847
				$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'])));
4848
				log_error(sprintf($txt['hook_fail_call_to'], $string, $absPath), 'general');
4849
			}
4850
			// "Assume" the file resides on $boarddir somewhere...
4851
			else
4852
				log_error(sprintf($txt['hook_fail_call_to'], $function, $boarddir), 'general');
4853
		}
4854
	}
4855
4856
	return $results;
4857
}
4858
4859
/**
4860
 * Add a function for integration hook.
4861
 * does nothing if the function is already added.
4862
 *
4863
 * @param string $hook The complete hook name.
4864
 * @param string $function The function name. Can be a call to a method via Class::method.
4865
 * @param bool $permanent If true, updates the value in settings table.
4866
 * @param string $file The file. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
4867
 * @param bool $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
4868
 */
4869
function add_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
4870
{
4871
	global $smcFunc, $modSettings;
4872
4873
	// Any objects?
4874
	if ($object)
4875
		$function = $function . '#';
4876
4877
	// Any files  to load?
4878
	if (!empty($file) && is_string($file))
4879
		$function = $file . (!empty($function) ? '|' . $function : '');
4880
4881
	// Get the correct string.
4882
	$integration_call = $function;
4883
4884
	// Is it going to be permanent?
4885
	if ($permanent)
4886
	{
4887
		$request = $smcFunc['db_query']('', '
4888
			SELECT value
4889
			FROM {db_prefix}settings
4890
			WHERE variable = {string:variable}',
4891
			array(
4892
				'variable' => $hook,
4893
			)
4894
		);
4895
		list ($current_functions) = $smcFunc['db_fetch_row']($request);
4896
		$smcFunc['db_free_result']($request);
4897
4898
		if (!empty($current_functions))
4899
		{
4900
			$current_functions = explode(',', $current_functions);
4901
			if (in_array($integration_call, $current_functions))
4902
				return;
4903
4904
			$permanent_functions = array_merge($current_functions, array($integration_call));
4905
		}
4906
		else
4907
			$permanent_functions = array($integration_call);
4908
4909
		updateSettings(array($hook => implode(',', $permanent_functions)));
4910
	}
4911
4912
	// Make current function list usable.
4913
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
4914
4915
	// Do nothing, if it's already there.
4916
	if (in_array($integration_call, $functions))
4917
		return;
4918
4919
	$functions[] = $integration_call;
4920
	$modSettings[$hook] = implode(',', $functions);
4921
}
4922
4923
/**
4924
 * Remove an integration hook function.
4925
 * Removes the given function from the given hook.
4926
 * Does nothing if the function is not available.
4927
 *
4928
 * @param string $hook The complete hook name.
4929
 * @param string $function The function name. Can be a call to a method via Class::method.
4930
 * @param boolean $permanent Irrelevant for the function itself but need to declare it to match
4931
 * @param string $file The filename. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
4932
 * @param boolean $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
4933
 * @see add_integration_function
4934
 */
4935
function remove_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
4936
{
4937
	global $smcFunc, $modSettings;
4938
4939
	// Any objects?
4940
	if ($object)
4941
		$function = $function . '#';
4942
4943
	// Any files  to load?
4944
	if (!empty($file) && is_string($file))
4945
		$function = $file . '|' . $function;
4946
4947
	// Get the correct string.
4948
	$integration_call = $function;
4949
4950
	// Get the permanent functions.
4951
	$request = $smcFunc['db_query']('', '
4952
		SELECT value
4953
		FROM {db_prefix}settings
4954
		WHERE variable = {string:variable}',
4955
		array(
4956
			'variable' => $hook,
4957
		)
4958
	);
4959
	list ($current_functions) = $smcFunc['db_fetch_row']($request);
4960
	$smcFunc['db_free_result']($request);
4961
4962
	if (!empty($current_functions))
4963
	{
4964
		$current_functions = explode(',', $current_functions);
4965
4966
		if (in_array($integration_call, $current_functions))
4967
			updateSettings(array($hook => implode(',', array_diff($current_functions, array($integration_call)))));
4968
	}
4969
4970
	// Turn the function list into something usable.
4971
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
4972
4973
	// You can only remove it if it's available.
4974
	if (!in_array($integration_call, $functions))
4975
		return;
4976
4977
	$functions = array_diff($functions, array($integration_call));
4978
	$modSettings[$hook] = implode(',', $functions);
4979
}
4980
4981
/**
4982
 * Receives a string and tries to figure it out if its a method or a function.
4983
 * If a method is found, it looks for a "#" which indicates SMF should create a new instance of the given class.
4984
 * Checks the string/array for is_callable() and return false/fatal_lang_error is the given value results in a non callable string/array.
4985
 * Prepare and returns a callable depending on the type of method/function found.
4986
 *
4987
 * @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)
4988
 * @param boolean $return If true, the function will not call the function/method but instead will return the formatted string.
4989
 * @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.
4990
 */
4991
function call_helper($string, $return = false)
4992
{
4993
	global $context, $smcFunc, $txt, $db_show_debug;
4994
4995
	// Really?
4996
	if (empty($string))
4997
		return false;
4998
4999
	// An array? should be a "callable" array IE array(object/class, valid_callable).
5000
	// A closure? should be a callable one.
5001
	if (is_array($string) || $string instanceof Closure)
5002
		return $return ? $string : (is_callable($string) ? call_user_func($string) : false);
5003
5004
	// No full objects, sorry! pass a method or a property instead!
5005
	if (is_object($string))
5006
		return false;
5007
5008
	// Stay vitaminized my friends...
5009
	$string = $smcFunc['htmlspecialchars']($smcFunc['htmltrim']($string));
5010
5011
	// Is there a file to load?
5012
	$string = load_file($string);
5013
5014
	// Loaded file failed
5015
	if (empty($string))
5016
		return false;
5017
5018
	// Found a method.
5019
	if (strpos($string, '::') !== false)
5020
	{
5021
		list ($class, $method) = explode('::', $string);
5022
5023
		// Check if a new object will be created.
5024
		if (strpos($method, '#') !== false)
5025
		{
5026
			// Need to remove the # thing.
5027
			$method = str_replace('#', '', $method);
5028
5029
			// Don't need to create a new instance for every method.
5030
			if (empty($context['instances'][$class]) || !($context['instances'][$class] instanceof $class))
5031
			{
5032
				$context['instances'][$class] = new $class;
5033
5034
				// Add another one to the list.
5035
				if ($db_show_debug === true)
5036
				{
5037
					if (!isset($context['debug']['instances']))
5038
						$context['debug']['instances'] = array();
5039
5040
					$context['debug']['instances'][$class] = $class;
5041
				}
5042
			}
5043
5044
			$func = array($context['instances'][$class], $method);
5045
		}
5046
5047
		// Right then. This is a call to a static method.
5048
		else
5049
			$func = array($class, $method);
5050
	}
5051
5052
	// Nope! just a plain regular function.
5053
	else
5054
		$func = $string;
5055
5056
	// We can't call this helper, but we want to silently ignore this.
5057
	if (!is_callable($func, false, $callable_name) && !empty($context['ignore_hook_errors']))
5058
		return false;
5059
	// Right, we got what we need, time to do some checks.
5060
	elseif (!is_callable($func, false, $callable_name))
5061
	{
5062
		loadLanguage('Errors');
5063
		log_error(sprintf($txt['sub_action_fail'], $callable_name), 'general');
5064
5065
		// Gotta tell everybody.
5066
		return false;
5067
	}
5068
5069
	// Everything went better than expected.
5070
	else
5071
	{
5072
		// What are we gonna do about it?
5073
		if ($return)
5074
			return $func;
5075
5076
		// If this is a plain function, avoid the heat of calling call_user_func().
5077
		else
5078
		{
5079
			if (is_array($func))
5080
				call_user_func($func);
5081
5082
			else
5083
				$func();
5084
		}
5085
	}
5086
}
5087
5088
/**
5089
 * Receives a string and tries to figure it out if it contains info to load a file.
5090
 * Checks for a | (pipe) symbol and tries to load a file with the info given.
5091
 * 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.
5092
 *
5093
 * @param string $string The string containing a valid format.
5094
 * @return string|boolean The given string with the pipe and file info removed. Boolean false if the file couldn't be loaded.
5095
 */
5096
function load_file($string)
5097
{
5098
	global $sourcedir, $txt, $boarddir, $settings;
5099
5100
	if (empty($string))
5101
		return false;
5102
5103
	if (strpos($string, '|') !== false)
5104
	{
5105
		list ($file, $string) = explode('|', $string);
5106
5107
		// Match the wildcards to their regular vars.
5108
		if (empty($settings['theme_dir']))
5109
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
5110
5111
		else
5112
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
5113
5114
		// Load the file if it can be loaded.
5115
		if (file_exists($absPath))
5116
			require_once($absPath);
5117
5118
		// No? try a fallback to $sourcedir
5119
		else
5120
		{
5121
			$absPath = $sourcedir . '/' . $file;
5122
5123
			if (file_exists($absPath))
5124
				require_once($absPath);
5125
5126
			// Sorry, can't do much for you at this point.
5127
			else
5128
			{
5129
				loadLanguage('Errors');
5130
				log_error(sprintf($txt['hook_fail_loading_file'], $absPath), 'general');
5131
5132
				// File couldn't be loaded.
5133
				return false;
5134
			}
5135
		}
5136
	}
5137
5138
	return $string;
5139
}
5140
5141
/**
5142
 * Get the contents of a URL, irrespective of allow_url_fopen.
5143
 *
5144
 * - reads the contents of an http or ftp address and returns the page in a string
5145
 * - will accept up to 3 page redirections (redirectio_level in the function call is private)
5146
 * - if post_data is supplied, the value and length is posted to the given url as form data
5147
 * - URL must be supplied in lowercase
5148
 *
5149
 * @param string $url The URL
5150
 * @param string $post_data The data to post to the given URL
5151
 * @param bool $keep_alive Whether to send keepalive info
5152
 * @param int $redirection_level How many levels of redirection
5153
 * @return string|false The fetched data or false on failure
5154
 */
5155
function fetch_web_data($url, $post_data = '', $keep_alive = false, $redirection_level = 0)
5156
{
5157
	global $webmaster_email, $sourcedir;
5158
	static $keep_alive_dom = null, $keep_alive_fp = null;
5159
5160
	preg_match('~^(http|ftp)(s)?://([^/:]+)(:(\d+))?(.+)$~', $url, $match);
5161
5162
	// No scheme? No data for you!
5163
	if (empty($match[1]))
5164
		return false;
5165
5166
	// An FTP url. We should try connecting and RETRieving it...
5167
	elseif ($match[1] == 'ftp')
5168
	{
5169
		// Include the file containing the ftp_connection class.
5170
		require_once($sourcedir . '/Class-Package.php');
5171
5172
		// Establish a connection and attempt to enable passive mode.
5173
		$ftp = new ftp_connection(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? 21 : $match[5], 'anonymous', $webmaster_email);
5174
		if ($ftp->error !== false || !$ftp->passive())
0 ignored issues
show
introduced by
The condition $ftp->error !== false is always true.
Loading history...
5175
			return false;
5176
5177
		// I want that one *points*!
5178
		fwrite($ftp->connection, 'RETR ' . $match[6] . "\r\n");
5179
5180
		// Since passive mode worked (or we would have returned already!) open the connection.
5181
		$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...
5182
		if (!$fp)
5183
			return false;
5184
5185
		// The server should now say something in acknowledgement.
5186
		$ftp->check_response(150);
5187
5188
		$data = '';
5189
		while (!feof($fp))
5190
			$data .= fread($fp, 4096);
5191
		fclose($fp);
5192
5193
		// All done, right?  Good.
5194
		$ftp->check_response(226);
5195
		$ftp->close();
5196
	}
5197
5198
	// This is more likely; a standard HTTP URL.
5199
	elseif (isset($match[1]) && $match[1] == 'http')
5200
	{
5201
		// First try to use fsockopen, because it is fastest.
5202
		if ($keep_alive && $match[3] == $keep_alive_dom)
5203
			$fp = $keep_alive_fp;
5204
		if (empty($fp))
5205
		{
5206
			// Open the socket on the port we want...
5207
			$fp = @fsockopen(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? ($match[2] ? 443 : 80) : $match[5], $err, $err, 5);
5208
		}
5209
		if (!empty($fp))
5210
		{
5211
			if ($keep_alive)
5212
			{
5213
				$keep_alive_dom = $match[3];
5214
				$keep_alive_fp = $fp;
5215
			}
5216
5217
			// I want this, from there, and I'm not going to be bothering you for more (probably.)
5218
			if (empty($post_data))
5219
			{
5220
				fwrite($fp, 'GET ' . ($match[6] !== '/' ? str_replace(' ', '%20', $match[6]) : '') . ' HTTP/1.0' . "\r\n");
5221
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
5222
				fwrite($fp, 'user-agent: PHP/SMF' . "\r\n");
5223
				if ($keep_alive)
5224
					fwrite($fp, 'connection: Keep-Alive' . "\r\n\r\n");
5225
				else
5226
					fwrite($fp, 'connection: close' . "\r\n\r\n");
5227
			}
5228
			else
5229
			{
5230
				fwrite($fp, 'POST ' . ($match[6] !== '/' ? $match[6] : '') . ' HTTP/1.0' . "\r\n");
5231
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
5232
				fwrite($fp, 'user-agent: PHP/SMF' . "\r\n");
5233
				if ($keep_alive)
5234
					fwrite($fp, 'connection: Keep-Alive' . "\r\n");
5235
				else
5236
					fwrite($fp, 'connection: close' . "\r\n");
5237
				fwrite($fp, 'content-type: application/x-www-form-urlencoded' . "\r\n");
5238
				fwrite($fp, 'content-length: ' . strlen($post_data) . "\r\n\r\n");
5239
				fwrite($fp, $post_data);
5240
			}
5241
5242
			$response = fgets($fp, 768);
5243
5244
			// Redirect in case this location is permanently or temporarily moved.
5245
			if ($redirection_level < 3 && preg_match('~^HTTP/\S+\s+30[127]~i', $response) === 1)
5246
			{
5247
				$header = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $header is dead and can be removed.
Loading history...
5248
				$location = '';
5249
				while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
5250
					if (stripos($header, 'location:') !== false)
5251
						$location = trim(substr($header, strpos($header, ':') + 1));
5252
5253
				if (empty($location))
5254
					return false;
5255
				else
5256
				{
5257
					if (!$keep_alive)
5258
						fclose($fp);
5259
					return fetch_web_data($location, $post_data, $keep_alive, $redirection_level + 1);
5260
				}
5261
			}
5262
5263
			// Make sure we get a 200 OK.
5264
			elseif (preg_match('~^HTTP/\S+\s+20[01]~i', $response) === 0)
5265
				return false;
5266
5267
			// Skip the headers...
5268
			while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
5269
			{
5270
				if (preg_match('~content-length:\s*(\d+)~i', $header, $match) != 0)
5271
					$content_length = $match[1];
5272
				elseif (preg_match('~connection:\s*close~i', $header) != 0)
5273
				{
5274
					$keep_alive_dom = null;
5275
					$keep_alive = false;
5276
				}
5277
5278
				continue;
5279
			}
5280
5281
			$data = '';
5282
			if (isset($content_length))
5283
			{
5284
				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...
5285
					$data .= fread($fp, $content_length - strlen($data));
5286
			}
5287
			else
5288
			{
5289
				while (!feof($fp))
5290
					$data .= fread($fp, 4096);
5291
			}
5292
5293
			if (!$keep_alive)
5294
				fclose($fp);
5295
		}
5296
5297
		// If using fsockopen didn't work, try to use cURL if available.
5298
		elseif (function_exists('curl_init'))
5299
		{
5300
			// Include the file containing the curl_fetch_web_data class.
5301
			require_once($sourcedir . '/Class-CurlFetchWeb.php');
5302
5303
			$fetch_data = new curl_fetch_web_data();
5304
			$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

5304
			$fetch_data->get_url_data($url, /** @scrutinizer ignore-type */ $post_data);
Loading history...
5305
5306
			// no errors and a 200 result, then we have a good dataset, well we at least have data. ;)
5307
			if ($fetch_data->result('code') == 200 && !$fetch_data->result('error'))
5308
				$data = $fetch_data->result('body');
5309
			else
5310
				return false;
5311
		}
5312
5313
		// Neither fsockopen nor curl are available. Well, phooey.
5314
		else
5315
			return false;
5316
	}
5317
	else
5318
	{
5319
		// Umm, this shouldn't happen?
5320
		trigger_error('fetch_web_data(): Bad URL', E_USER_NOTICE);
5321
		$data = false;
5322
	}
5323
5324
	return $data;
5325
}
5326
5327
/**
5328
 * Prepares an array of "likes" info for the topic specified by $topic
5329
 *
5330
 * @param integer $topic The topic ID to fetch the info from.
5331
 * @return array An array of IDs of messages in the specified topic that the current user likes
5332
 */
5333
function prepareLikesContext($topic)
5334
{
5335
	global $user_info, $smcFunc;
5336
5337
	// Make sure we have something to work with.
5338
	if (empty($topic))
5339
		return array();
5340
5341
	// We already know the number of likes per message, we just want to know whether the current user liked it or not.
5342
	$user = $user_info['id'];
5343
	$cache_key = 'likes_topic_' . $topic . '_' . $user;
5344
	$ttl = 180;
5345
5346
	if (($temp = cache_get_data($cache_key, $ttl)) === null)
0 ignored issues
show
introduced by
The condition $temp = cache_get_data($cache_key, $ttl) === null is always false.
Loading history...
5347
	{
5348
		$temp = array();
5349
		$request = $smcFunc['db_query']('', '
5350
			SELECT content_id
5351
			FROM {db_prefix}user_likes AS l
5352
				INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg)
5353
			WHERE l.id_member = {int:current_user}
5354
				AND l.content_type = {literal:msg}
5355
				AND m.id_topic = {int:topic}',
5356
			array(
5357
				'current_user' => $user,
5358
				'topic' => $topic,
5359
			)
5360
		);
5361
		while ($row = $smcFunc['db_fetch_assoc']($request))
5362
			$temp[] = (int) $row['content_id'];
5363
5364
		cache_put_data($cache_key, $temp, $ttl);
5365
	}
5366
5367
	return $temp;
5368
}
5369
5370
/**
5371
 * Microsoft uses their own character set Code Page 1252 (CP1252), which is a
5372
 * superset of ISO 8859-1, defining several characters between DEC 128 and 159
5373
 * that are not normally displayable.  This converts the popular ones that
5374
 * appear from a cut and paste from windows.
5375
 *
5376
 * @param string $string The string
5377
 * @return string The sanitized string
5378
 */
5379
function sanitizeMSCutPaste($string)
5380
{
5381
	global $context;
5382
5383
	if (empty($string))
5384
		return $string;
5385
5386
	// UTF-8 occurences of MS special characters
5387
	$findchars_utf8 = array(
5388
		"\xe2\x80\x9a",	// single low-9 quotation mark
5389
		"\xe2\x80\x9e",	// double low-9 quotation mark
5390
		"\xe2\x80\xa6",	// horizontal ellipsis
5391
		"\xe2\x80\x98",	// left single curly quote
5392
		"\xe2\x80\x99",	// right single curly quote
5393
		"\xe2\x80\x9c",	// left double curly quote
5394
		"\xe2\x80\x9d",	// right double curly quote
5395
		"\xe2\x80\x93",	// en dash
5396
		"\xe2\x80\x94",	// em dash
5397
	);
5398
5399
	// windows 1252 / iso equivalents
5400
	$findchars_iso = array(
5401
		chr(130),
5402
		chr(132),
5403
		chr(133),
5404
		chr(145),
5405
		chr(146),
5406
		chr(147),
5407
		chr(148),
5408
		chr(150),
5409
		chr(151),
5410
	);
5411
5412
	// safe replacements
5413
	$replacechars = array(
5414
		',',	// &sbquo;
5415
		',,',	// &bdquo;
5416
		'...',	// &hellip;
5417
		"'",	// &lsquo;
5418
		"'",	// &rsquo;
5419
		'"',	// &ldquo;
5420
		'"',	// &rdquo;
5421
		'-',	// &ndash;
5422
		'--',	// &mdash;
5423
	);
5424
5425
	if ($context['utf8'])
5426
		$string = str_replace($findchars_utf8, $replacechars, $string);
5427
	else
5428
		$string = str_replace($findchars_iso, $replacechars, $string);
5429
5430
	return $string;
5431
}
5432
5433
/**
5434
 * Decode numeric html entities to their ascii or UTF8 equivalent character.
5435
 *
5436
 * Callback function for preg_replace_callback in subs-members
5437
 * Uses capture group 2 in the supplied array
5438
 * Does basic scan to ensure characters are inside a valid range
5439
 *
5440
 * @param array $matches An array of matches (relevant info should be the 3rd item)
5441
 * @return string A fixed string
5442
 */
5443
function replaceEntities__callback($matches)
5444
{
5445
	global $context;
5446
5447
	if (!isset($matches[2]))
5448
		return '';
5449
5450
	$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

5450
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5451
5452
	// remove left to right / right to left overrides
5453
	if ($num === 0x202D || $num === 0x202E)
5454
		return '';
5455
5456
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
5457
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
5458
		return '&#' . $num . ';';
5459
5460
	if (empty($context['utf8']))
5461
	{
5462
		// no control characters
5463
		if ($num < 0x20)
5464
			return '';
5465
		// text is text
5466
		elseif ($num < 0x80)
5467
			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

5467
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5468
		// all others get html-ised
5469
		else
5470
			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

5470
			return '&#' . /** @scrutinizer ignore-type */ $matches[2] . ';';
Loading history...
5471
	}
5472
	else
5473
	{
5474
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
5475
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
5476
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
5477
			return '';
5478
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
5479
		elseif ($num < 0x80)
5480
			return chr($num);
5481
		// <0x800 (2048)
5482
		elseif ($num < 0x800)
5483
			return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5484
		// < 0x10000 (65536)
5485
		elseif ($num < 0x10000)
5486
			return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5487
		// <= 0x10FFFF (1114111)
5488
		else
5489
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5490
	}
5491
}
5492
5493
/**
5494
 * Converts html entities to utf8 equivalents
5495
 *
5496
 * Callback function for preg_replace_callback
5497
 * Uses capture group 1 in the supplied array
5498
 * Does basic checks to keep characters inside a viewable range.
5499
 *
5500
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
5501
 * @return string The fixed string
5502
 */
5503
function fixchar__callback($matches)
5504
{
5505
	if (!isset($matches[1]))
5506
		return '';
5507
5508
	$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

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

5516
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5517
	// <0x800 (2048)
5518
	elseif ($num < 0x800)
5519
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5520
	// < 0x10000 (65536)
5521
	elseif ($num < 0x10000)
5522
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5523
	// <= 0x10FFFF (1114111)
5524
	else
5525
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5526
}
5527
5528
/**
5529
 * Strips out invalid html entities, replaces others with html style &#123; codes
5530
 *
5531
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
5532
 * strpos, strlen, substr etc
5533
 *
5534
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
5535
 * @return string The fixed string
5536
 */
5537
function entity_fix__callback($matches)
5538
{
5539
	if (!isset($matches[2]))
5540
		return '';
5541
5542
	$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

5542
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5543
5544
	// we don't allow control characters, characters out of range, byte markers, etc
5545
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
5546
		return '';
5547
	else
5548
		return '&#' . $num . ';';
5549
}
5550
5551
/**
5552
 * Return a Gravatar URL based on
5553
 * - the supplied email address,
5554
 * - the global maximum rating,
5555
 * - the global default fallback,
5556
 * - maximum sizes as set in the admin panel.
5557
 *
5558
 * It is SSL aware, and caches most of the parameters.
5559
 *
5560
 * @param string $email_address The user's email address
5561
 * @return string The gravatar URL
5562
 */
5563
function get_gravatar_url($email_address)
5564
{
5565
	global $modSettings, $smcFunc;
5566
	static $url_params = null;
5567
5568
	if ($url_params === null)
5569
	{
5570
		$ratings = array('G', 'PG', 'R', 'X');
5571
		$defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank');
5572
		$url_params = array();
5573
		if (!empty($modSettings['gravatarMaxRating']) && in_array($modSettings['gravatarMaxRating'], $ratings))
5574
			$url_params[] = 'rating=' . $modSettings['gravatarMaxRating'];
5575
		if (!empty($modSettings['gravatarDefault']) && in_array($modSettings['gravatarDefault'], $defaults))
5576
			$url_params[] = 'default=' . $modSettings['gravatarDefault'];
5577
		if (!empty($modSettings['avatar_max_width_external']))
5578
			$size_string = (int) $modSettings['avatar_max_width_external'];
5579
		if (!empty($modSettings['avatar_max_height_external']) && !empty($size_string))
5580
			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...
5581
				$size_string = $modSettings['avatar_max_height_external'];
5582
5583
		if (!empty($size_string))
5584
			$url_params[] = 's=' . $size_string;
5585
	}
5586
	$http_method = !empty($modSettings['force_ssl']) ? 'https://secure' : 'http://www';
5587
5588
	return $http_method . '.gravatar.com/avatar/' . md5($smcFunc['strtolower']($email_address)) . '?' . implode('&', $url_params);
5589
}
5590
5591
/**
5592
 * Get a list of timezones.
5593
 *
5594
 * @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'.
5595
 * @return array An array of timezone info.
5596
 */
5597
function smf_list_timezones($when = 'now')
5598
{
5599
	global $smcFunc, $modSettings, $tztxt, $txt;
5600
	static $timezones = null, $lastwhen = null;
5601
5602
	// No point doing this over if we already did it once
5603
	if (!empty($timezones) && $when == $lastwhen)
5604
		return $timezones;
5605
	else
5606
		$lastwhen = $when;
5607
5608
	// Parseable datetime string?
5609
	if (is_int($timestamp = strtotime($when)))
0 ignored issues
show
introduced by
The condition is_int($timestamp = strtotime($when)) is always true.
Loading history...
5610
		$when = $timestamp;
5611
5612
	// A Unix timestamp?
5613
	elseif (is_numeric($when))
5614
		$when = intval($when);
5615
5616
	// Invalid value? Just get current Unix timestamp.
5617
	else
5618
		$when = time();
5619
5620
	// We'll need these too
5621
	$date_when = date_create('@' . $when);
5622
	$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

5622
	$later = (int) date_format(date_add(/** @scrutinizer ignore-type */ $date_when, date_interval_create_from_date_string('1 year')), 'U');
Loading history...
5623
5624
	// Load up any custom time zone descriptions we might have
5625
	loadLanguage('Timezones');
5626
5627
	// Should we put time zones from certain countries at the top of the list?
5628
	$priority_countries = !empty($modSettings['timezone_priority_countries']) ? explode(',', $modSettings['timezone_priority_countries']) : array();
5629
	$priority_tzids = array();
5630
	foreach ($priority_countries as $country)
5631
	{
5632
		$country_tzids = @timezone_identifiers_list(DateTimeZone::PER_COUNTRY, strtoupper(trim($country)));
5633
		if (!empty($country_tzids))
5634
			$priority_tzids = array_merge($priority_tzids, $country_tzids);
5635
	}
5636
5637
	// Antarctic research stations should be listed last, unless you're running a penguin forum
5638
	$low_priority_tzids = !in_array('AQ', $priority_countries) ? timezone_identifiers_list(DateTimeZone::ANTARCTICA) : array();
5639
5640
	// Process the preferred timezones first, then the normal ones, then the low priority ones.
5641
	$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 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

5641
	$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...
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

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

5641
	$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...
5642
5643
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
5644
	foreach ($tzids as $tzid)
5645
	{
5646
		// We don't want UTC right now
5647
		if ($tzid == 'UTC')
5648
			continue;
5649
5650
		$tz = timezone_open($tzid);
5651
5652
		// First, get the set of transition rules for this tzid
5653
		$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

5653
		$tzinfo = timezone_transitions_get(/** @scrutinizer ignore-type */ $tz, $when, $later);
Loading history...
5654
5655
		// Use the entire set of transition rules as the array *key* so we can avoid duplicates
5656
		$tzkey = serialize($tzinfo);
5657
5658
		// Next, get the geographic info for this tzid
5659
		$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

5659
		$tzgeo = timezone_location_get(/** @scrutinizer ignore-type */ $tz);
Loading history...
5660
5661
		// Don't overwrite our preferred tzids
5662
		if (empty($zones[$tzkey]['tzid']))
5663
		{
5664
			$zones[$tzkey]['tzid'] = $tzid;
5665
			$zones[$tzkey]['abbr'] = $tzinfo[0]['abbr'];
5666
		}
5667
5668
		// A time zone from a prioritized country?
5669
		if (in_array($tzid, $priority_tzids))
5670
			$priority_zones[$tzkey] = true;
5671
5672
		// Keep track of the location and offset for this tzid
5673
		if (!empty($txt[$tzid]))
5674
			$zones[$tzkey]['locations'][] = $txt[$tzid];
5675
		else
5676
		{
5677
			$tzid_parts = explode('/', $tzid);
5678
			$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
5679
		}
5680
		$offsets[$tzkey] = $tzinfo[0]['offset'];
5681
		$longitudes[$tzkey] = empty($longitudes[$tzkey]) ? $tzgeo['longitude'] : $longitudes[$tzkey];
5682
	}
5683
5684
	// Sort by offset then longitude
5685
	array_multisort($offsets, SORT_ASC, SORT_NUMERIC, $longitudes, SORT_ASC, SORT_NUMERIC, $zones);
0 ignored issues
show
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...
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...
5686
5687
	// Build the final array of formatted values
5688
	$priority_timezones = array();
5689
	$timezones = array();
5690
	foreach ($zones as $tzkey => $tzvalue)
5691
	{
5692
		date_timezone_set($date_when, timezone_open($tzvalue['tzid']));
0 ignored issues
show
Bug introduced by
It seems like $date_when can also be of type false; however, parameter $object of date_timezone_set() does only seem to accept DateTime, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

5692
		date_timezone_set($date_when, /** @scrutinizer ignore-type */ timezone_open($tzvalue['tzid']));
Loading history...
5693
5694
		// Use the custom description, if there is one
5695
		if (!empty($tztxt[$tzvalue['tzid']]))
5696
			$desc = $tztxt[$tzvalue['tzid']];
5697
		// Otherwise, use the list of locations (max 5, so things don't get silly)
5698
		else
5699
			$desc = implode(', ', array_slice(array_unique($tzvalue['locations']), 0, 5)) . (count($tzvalue['locations']) > 5 ? ', ' . $txt['etc'] : '');
5700
5701
		// Show the UTC offset and the abbreviation, if it's something like 'MST' and not '-06'
5702
		$desc = '[UTC' . date_format($date_when, 'P') . '] - ' . (!strspn($tzvalue['abbr'], '+-') ? $tzvalue['abbr'] . ' - ' : '') . $desc;
5703
5704
		if (isset($priority_zones[$tzkey]))
5705
			$priority_timezones[$tzvalue['tzid']] = $desc;
5706
		else
5707
			$timezones[$tzvalue['tzid']] = $desc;
5708
	}
5709
5710
	if (!empty($priority_timezones))
5711
		$priority_timezones[] = '-----';
5712
5713
	$timezones = array_merge(
5714
		$priority_timezones,
5715
		array('' => '(Forum Default)', 'UTC' => 'UTC - ' . $tztxt['UTC'], '-----'),
5716
		$timezones
5717
	);
5718
5719
	return $timezones;
5720
}
5721
5722
/**
5723
 * @param string $ip_address An IP address in IPv4, IPv6 or decimal notation
5724
 * @return string|false The IP address in binary or false
5725
 */
5726
function inet_ptod($ip_address)
5727
{
5728
	if (!isValidIP($ip_address))
5729
		return $ip_address;
5730
5731
	$bin = inet_pton($ip_address);
5732
	return $bin;
5733
}
5734
5735
/**
5736
 * @param string $bin An IP address in IPv4, IPv6 (Either string (postgresql) or binary (other databases))
5737
 * @return string|false The IP address in presentation format or false on error
5738
 */
5739
function inet_dtop($bin)
5740
{
5741
	if (empty($bin))
5742
		return '';
5743
5744
	global $db_type;
5745
5746
	if ($db_type == 'postgresql')
5747
		return $bin;
5748
5749
	$ip_address = inet_ntop($bin);
5750
5751
	return $ip_address;
5752
}
5753
5754
/**
5755
 * Safe serialize() and unserialize() replacements
5756
 *
5757
 * @license Public Domain
5758
 *
5759
 * @author anthon (dot) pang (at) gmail (dot) com
5760
 */
5761
5762
/**
5763
 * Safe serialize() replacement. Recursive
5764
 * - output a strict subset of PHP's native serialized representation
5765
 * - does not serialize objects
5766
 *
5767
 * @param mixed $value
5768
 * @return string
5769
 */
5770
function _safe_serialize($value)
5771
{
5772
	if (is_null($value))
5773
		return 'N;';
5774
5775
	if (is_bool($value))
5776
		return 'b:' . (int) $value . ';';
5777
5778
	if (is_int($value))
5779
		return 'i:' . $value . ';';
5780
5781
	if (is_float($value))
5782
		return 'd:' . str_replace(',', '.', $value) . ';';
5783
5784
	if (is_string($value))
5785
		return 's:' . strlen($value) . ':"' . $value . '";';
5786
5787
	if (is_array($value))
5788
	{
5789
		$out = '';
5790
		foreach ($value as $k => $v)
5791
			$out .= _safe_serialize($k) . _safe_serialize($v);
5792
5793
		return 'a:' . count($value) . ':{' . $out . '}';
5794
	}
5795
5796
	// safe_serialize cannot serialize resources or objects.
5797
	return false;
5798
}
5799
5800
/**
5801
 * Wrapper for _safe_serialize() that handles exceptions and multibyte encoding issues.
5802
 *
5803
 * @param mixed $value
5804
 * @return string
5805
 */
5806
function safe_serialize($value)
5807
{
5808
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
5809
	if (function_exists('mb_internal_encoding') &&
5810
		(((int) ini_get('mbstring.func_overload')) & 2))
5811
	{
5812
		$mbIntEnc = mb_internal_encoding();
5813
		mb_internal_encoding('ASCII');
5814
	}
5815
5816
	$out = _safe_serialize($value);
5817
5818
	if (isset($mbIntEnc))
5819
		mb_internal_encoding($mbIntEnc);
5820
5821
	return $out;
5822
}
5823
5824
/**
5825
 * Safe unserialize() replacement
5826
 * - accepts a strict subset of PHP's native serialized representation
5827
 * - does not unserialize objects
5828
 *
5829
 * @param string $str
5830
 * @return mixed
5831
 * @throw Exception if $str is malformed or contains unsupported types (e.g., resources, objects)
5832
 */
5833
function _safe_unserialize($str)
5834
{
5835
	// Input  is not a string.
5836
	if (empty($str) || !is_string($str))
5837
		return false;
5838
5839
	$stack = array();
5840
	$expected = array();
5841
5842
	/*
5843
	 * states:
5844
	 *   0 - initial state, expecting a single value or array
5845
	 *   1 - terminal state
5846
	 *   2 - in array, expecting end of array or a key
5847
	 *   3 - in array, expecting value or another array
5848
	 */
5849
	$state = 0;
5850
	while ($state != 1)
5851
	{
5852
		$type = isset($str[0]) ? $str[0] : '';
5853
		if ($type == '}')
5854
			$str = substr($str, 1);
5855
5856
		elseif ($type == 'N' && $str[1] == ';')
5857
		{
5858
			$value = null;
5859
			$str = substr($str, 2);
5860
		}
5861
		elseif ($type == 'b' && preg_match('/^b:([01]);/', $str, $matches))
5862
		{
5863
			$value = $matches[1] == '1' ? true : false;
5864
			$str = substr($str, 4);
5865
		}
5866
		elseif ($type == 'i' && preg_match('/^i:(-?[0-9]+);(.*)/s', $str, $matches))
5867
		{
5868
			$value = (int) $matches[1];
5869
			$str = $matches[2];
5870
		}
5871
		elseif ($type == 'd' && preg_match('/^d:(-?[0-9]+\.?[0-9]*(E[+-][0-9]+)?);(.*)/s', $str, $matches))
5872
		{
5873
			$value = (float) $matches[1];
5874
			$str = $matches[3];
5875
		}
5876
		elseif ($type == 's' && preg_match('/^s:([0-9]+):"(.*)/s', $str, $matches) && substr($matches[2], (int) $matches[1], 2) == '";')
5877
		{
5878
			$value = substr($matches[2], 0, (int) $matches[1]);
5879
			$str = substr($matches[2], (int) $matches[1] + 2);
5880
		}
5881
		elseif ($type == 'a' && preg_match('/^a:([0-9]+):{(.*)/s', $str, $matches))
5882
		{
5883
			$expectedLength = (int) $matches[1];
5884
			$str = $matches[2];
5885
		}
5886
5887
		// Object or unknown/malformed type.
5888
		else
5889
			return false;
5890
5891
		switch ($state)
5892
		{
5893
			case 3: // In array, expecting value or another array.
5894
				if ($type == 'a')
5895
				{
5896
					$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...
5897
					$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...
5898
					$list = &$list[$key];
5899
					$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...
5900
					$state = 2;
5901
					break;
5902
				}
5903
				if ($type != '}')
5904
				{
5905
					$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...
5906
					$state = 2;
5907
					break;
5908
				}
5909
5910
				// Missing array value.
5911
				return false;
5912
5913
			case 2: // in array, expecting end of array or a key
5914
				if ($type == '}')
5915
				{
5916
					// Array size is less than expected.
5917
					if (count($list) < end($expected))
5918
						return false;
5919
5920
					unset($list);
5921
					$list = &$stack[count($stack) - 1];
5922
					array_pop($stack);
5923
5924
					// Go to terminal state if we're at the end of the root array.
5925
					array_pop($expected);
5926
5927
					if (count($expected) == 0)
5928
						$state = 1;
5929
5930
					break;
5931
				}
5932
5933
				if ($type == 'i' || $type == 's')
5934
				{
5935
					// Array size exceeds expected length.
5936
					if (count($list) >= end($expected))
5937
						return false;
5938
5939
					$key = $value;
5940
					$state = 3;
5941
					break;
5942
				}
5943
5944
				// Illegal array index type.
5945
				return false;
5946
5947
			// Expecting array or value.
5948
			case 0:
5949
				if ($type == 'a')
5950
				{
5951
					$data = array();
5952
					$list = &$data;
5953
					$expected[] = $expectedLength;
5954
					$state = 2;
5955
					break;
5956
				}
5957
5958
				if ($type != '}')
5959
				{
5960
					$data = $value;
5961
					$state = 1;
5962
					break;
5963
				}
5964
5965
				// Not in array.
5966
				return false;
5967
		}
5968
	}
5969
5970
	// Trailing data in input.
5971
	if (!empty($str))
5972
		return false;
5973
5974
	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...
5975
}
5976
5977
/**
5978
 * Wrapper for _safe_unserialize() that handles exceptions and multibyte encoding issue
5979
 *
5980
 * @param string $str
5981
 * @return mixed
5982
 */
5983
function safe_unserialize($str)
5984
{
5985
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
5986
	if (function_exists('mb_internal_encoding') &&
5987
		(((int) ini_get('mbstring.func_overload')) & 0x02))
5988
	{
5989
		$mbIntEnc = mb_internal_encoding();
5990
		mb_internal_encoding('ASCII');
5991
	}
5992
5993
	$out = _safe_unserialize($str);
5994
5995
	if (isset($mbIntEnc))
5996
		mb_internal_encoding($mbIntEnc);
5997
5998
	return $out;
5999
}
6000
6001
/**
6002
 * Tries different modes to make file/dirs writable. Wrapper function for chmod()
6003
 *
6004
 * @param string $file The file/dir full path.
6005
 * @param int $value Not needed, added for legacy reasons.
6006
 * @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.
6007
 */
6008
function smf_chmod($file, $value = 0)
6009
{
6010
	// No file? no checks!
6011
	if (empty($file))
6012
		return false;
6013
6014
	// Already writable?
6015
	if (is_writable($file))
6016
		return true;
6017
6018
	// Do we have a file or a dir?
6019
	$isDir = is_dir($file);
6020
	$isWritable = false;
6021
6022
	// Set different modes.
6023
	$chmodValues = $isDir ? array(0750, 0755, 0775, 0777) : array(0644, 0664, 0666);
6024
6025
	foreach ($chmodValues as $val)
6026
	{
6027
		// If it's writable, break out of the loop.
6028
		if (is_writable($file))
6029
		{
6030
			$isWritable = true;
6031
			break;
6032
		}
6033
6034
		else
6035
			@chmod($file, $val);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

6035
			/** @scrutinizer ignore-unhandled */ @chmod($file, $val);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
6036
	}
6037
6038
	return $isWritable;
6039
}
6040
6041
/**
6042
 * Wrapper function for json_decode() with error handling.
6043
 *
6044
 * @param string $json The string to decode.
6045
 * @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.
6046
 * @param bool $logIt To specify if the error will be logged if theres any.
6047
 * @return array Either an empty array or the decoded data as an array.
6048
 */
6049
function smf_json_decode($json, $returnAsArray = false, $logIt = true)
6050
{
6051
	global $txt;
6052
6053
	// Come on...
6054
	if (empty($json) || !is_string($json))
6055
		return array();
6056
6057
	$returnArray = @json_decode($json, $returnAsArray);
6058
6059
	// PHP 5.3 so no json_last_error_msg()
6060
	switch (json_last_error())
6061
	{
6062
		case JSON_ERROR_NONE:
6063
			$jsonError = false;
6064
			break;
6065
		case JSON_ERROR_DEPTH:
6066
			$jsonError = 'JSON_ERROR_DEPTH';
6067
			break;
6068
		case JSON_ERROR_STATE_MISMATCH:
6069
			$jsonError = 'JSON_ERROR_STATE_MISMATCH';
6070
			break;
6071
		case JSON_ERROR_CTRL_CHAR:
6072
			$jsonError = 'JSON_ERROR_CTRL_CHAR';
6073
			break;
6074
		case JSON_ERROR_SYNTAX:
6075
			$jsonError = 'JSON_ERROR_SYNTAX';
6076
			break;
6077
		case JSON_ERROR_UTF8:
6078
			$jsonError = 'JSON_ERROR_UTF8';
6079
			break;
6080
		default:
6081
			$jsonError = 'unknown';
6082
			break;
6083
	}
6084
6085
	// Something went wrong!
6086
	if (!empty($jsonError) && $logIt)
6087
	{
6088
		// Being a wrapper means we lost our smf_error_handler() privileges :(
6089
		$jsonDebug = debug_backtrace();
6090
		$jsonDebug = $jsonDebug[0];
6091
		loadLanguage('Errors');
6092
6093
		if (!empty($jsonDebug))
6094
			log_error($txt['json_' . $jsonError], 'critical', $jsonDebug['file'], $jsonDebug['line']);
6095
6096
		else
6097
			log_error($txt['json_' . $jsonError], 'critical');
6098
6099
		// Everyone expects an array.
6100
		return array();
6101
	}
6102
6103
	return $returnArray;
6104
}
6105
6106
/**
6107
 * Check the given String if he is a valid IPv4 or IPv6
6108
 * return true or false
6109
 *
6110
 * @param string $IPString
6111
 *
6112
 * @return bool
6113
 */
6114
function isValidIP($IPString)
6115
{
6116
	return filter_var($IPString, FILTER_VALIDATE_IP) !== false;
6117
}
6118
6119
/**
6120
 * Outputs a response.
6121
 * It assumes the data is already a string.
6122
 *
6123
 * @param string $data The data to print
6124
 * @param string $type The content type. Defaults to Json.
6125
 * @return void
6126
 */
6127
function smf_serverResponse($data = '', $type = 'content-type: application/json')
6128
{
6129
	global $db_show_debug, $modSettings;
6130
6131
	// Defensive programming anyone?
6132
	if (empty($data))
6133
		return false;
6134
6135
	// Don't need extra stuff...
6136
	$db_show_debug = false;
6137
6138
	// Kill anything else.
6139
	ob_end_clean();
6140
6141
	if (!empty($modSettings['CompressedOutput']))
6142
		@ob_start('ob_gzhandler');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for ob_start(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

6142
		/** @scrutinizer ignore-unhandled */ @ob_start('ob_gzhandler');

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
6143
6144
	else
6145
		ob_start();
6146
6147
	// Set the header.
6148
	header($type);
6149
6150
	// Echo!
6151
	echo $data;
6152
6153
	// Done.
6154
	obExit(false);
6155
}
6156
6157
/**
6158
 * Creates an optimized regex to match all known top level domains.
6159
 *
6160
 * The optimized regex is stored in $modSettings['tld_regex'].
6161
 *
6162
 * To update the stored version of the regex to use the latest list of valid TLDs from iana.org, set
6163
 * the $update parameter to true. Updating can take some time, based on network connectivity, so it
6164
 * should normally only be done by calling this function from a background or scheduled task.
6165
 *
6166
 * If $update is not true, but the regex is missing or invalid, the regex will be regenerated from a
6167
 * hard-coded list of TLDs. This regenerated regex will be overwritten on the next scheduled update.
6168
 *
6169
 * @param bool $update If true, fetch and process the latest official list of TLDs from iana.org.
6170
 */
6171
function set_tld_regex($update = false)
6172
{
6173
	global $sourcedir, $smcFunc, $modSettings;
6174
	static $done = false;
6175
6176
	// If we don't need to do anything, don't
6177
	if (!$update && $done)
6178
		return;
6179
6180
	// Should we get a new copy of the official list of TLDs?
6181
	if ($update)
6182
	{
6183
		$tlds = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt');
6184
6185
		// If the Internet Assigned Numbers Authority can't be reached, the Internet is GONE!
6186
		// We're probably running on a server hidden in a bunker deep underground to protect it from
6187
		// marauding bandits roaming on the surface. We don't want to waste precious electricity on
6188
		// pointlessly repeating background tasks, so we'll wait until the next regularly scheduled
6189
		// update to see if civilization has been restored.
6190
		if ($tlds === false)
6191
			$postapocalypticNightmare = true;
6192
	}
6193
	// If we aren't updating and the regex is valid, we're done
6194
	elseif (!empty($modSettings['tld_regex']) && @preg_match('~' . $modSettings['tld_regex'] . '~', null) !== false)
6195
	{
6196
		$done = true;
6197
		return;
6198
	}
6199
6200
	// If we successfully got an update, process the list into an array
6201
	if (!empty($tlds))
6202
	{
6203
		// Clean $tlds and convert it to an array
6204
		$tlds = array_filter(explode("\n", strtolower($tlds)), function($line)
6205
		{
6206
			$line = trim($line);
6207
			if (empty($line) || strpos($line, '#') !== false || strpos($line, ' ') !== false)
6208
				return false;
6209
			else
6210
				return true;
6211
		});
6212
6213
		// Convert Punycode to Unicode
6214
		require_once($sourcedir . '/Class-Punycode.php');
6215
		$Punycode = new Punycode();
6216
		$tlds = array_map(function($input) use ($Punycode)
6217
		{
6218
			return $Punycode->decode($input);
6219
		}, $tlds);
6220
	}
6221
	// Otherwise, use the 2012 list of gTLDs and ccTLDs for now and schedule a background update
6222
	else
6223
	{
6224
		$tlds = array('com', 'net', 'org', 'edu', 'gov', 'mil', 'aero', 'asia', 'biz', 'cat',
6225
			'coop', 'info', 'int', 'jobs', 'mobi', 'museum', 'name', 'post', 'pro', 'tel',
6226
			'travel', 'xxx', 'ac', 'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'an', 'ao', 'aq',
6227
			'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh',
6228
			'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cc',
6229
			'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 'co', 'cr', 'cs', 'cu', 'cv',
6230
			'cx', 'cy', 'cz', 'dd', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'ee', 'eg', 'eh',
6231
			'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr', 'ga', 'gb', 'gd', 'ge',
6232
			'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw',
6233
			'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu', 'id', 'ie', 'il', 'im', 'in', 'io', 'iq',
6234
			'ir', 'is', 'it', 'ja', 'je', 'jm', 'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn',
6235
			'kp', 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu',
6236
			'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp',
6237
			'mq', 'mr', 'ms', 'mt', 'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf',
6238
			'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph',
6239
			'pk', 'pl', 'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru',
6240
			'rw', 'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn',
6241
			'so', 'sr', 'ss', 'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th',
6242
			'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tp', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug',
6243
			'uk', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', 'ws', 'ye',
6244
			'yt', 'yu', 'za', 'zm', 'zw');
6245
6246
		// Schedule a background update, unless civilization has collapsed and/or we are having connectivity issues.
6247
		if (empty($postapocalypticNightmare))
6248
		{
6249
			$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
6250
				array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
6251
				array('$sourcedir/tasks/UpdateTldRegex.php', 'Update_TLD_Regex', '', 0), array()
6252
			);
6253
		}
6254
	}
6255
6256
	// Get an optimized regex to match all the TLDs
6257
	$tld_regex = build_regex($tlds);
6258
6259
	// Remember the new regex in $modSettings
6260
	updateSettings(array('tld_regex' => $tld_regex));
6261
6262
	// Redundant repetition is redundant
6263
	$done = true;
6264
}
6265
6266
/**
6267
 * Creates optimized regular expressions from an array of strings.
6268
 *
6269
 * An optimized regex built using this function will be much faster than a simple regex built using
6270
 * `implode('|', $strings)` --- anywhere from several times to several orders of magnitude faster.
6271
 *
6272
 * However, the time required to build the optimized regex is approximately equal to the time it
6273
 * takes to execute the simple regex. Therefore, it is only worth calling this function if the
6274
 * resulting regex will be used more than once.
6275
 *
6276
 * Because PHP places an upper limit on the allowed length of a regex, very large arrays of $strings
6277
 * may not fit in a single regex. Normally, the excess strings will simply be dropped. However, if
6278
 * the $returnArray parameter is set to true, this function will build as many regexes as necessary
6279
 * to accommodate everything in $strings and return them in an array. You will need to iterate
6280
 * through all elements of the returned array in order to test all possible matches.
6281
 *
6282
 * @param array $strings An array of strings to make a regex for.
6283
 * @param string $delim An optional delimiter character to pass to preg_quote().
6284
 * @param bool $returnArray If true, returns an array of regexes.
6285
 * @return string|array One or more regular expressions to match any of the input strings.
6286
 */
6287
function build_regex($strings, $delim = null, $returnArray = false)
6288
{
6289
	global $smcFunc;
6290
6291
	// The mb_* functions are faster than the $smcFunc ones, but may not be available
6292
	if (function_exists('mb_internal_encoding') && function_exists('mb_detect_encoding') && function_exists('mb_strlen') && function_exists('mb_substr'))
6293
	{
6294
		if (($string_encoding = mb_detect_encoding(implode(' ', $strings))) !== false)
6295
		{
6296
			$current_encoding = mb_internal_encoding();
6297
			mb_internal_encoding($string_encoding);
6298
		}
6299
6300
		$strlen = 'mb_strlen';
6301
		$substr = 'mb_substr';
6302
	}
6303
	else
6304
	{
6305
		$strlen = $smcFunc['strlen'];
6306
		$substr = $smcFunc['substr'];
6307
	}
6308
6309
	// This recursive function creates the index array from the strings
6310
	$add_string_to_index = function($string, $index) use (&$strlen, &$substr, &$add_string_to_index)
6311
	{
6312
		static $depth = 0;
6313
		$depth++;
6314
6315
		$first = $substr($string, 0, 1);
6316
6317
		if (empty($index[$first]))
6318
			$index[$first] = array();
6319
6320
		if ($strlen($string) > 1)
6321
		{
6322
			// Sanity check on recursion
6323
			if ($depth > 99)
6324
				$index[$first][$substr($string, 1)] = '';
6325
6326
			else
6327
				$index[$first] = $add_string_to_index($substr($string, 1), $index[$first]);
6328
		}
6329
		else
6330
			$index[$first][''] = '';
6331
6332
		$depth--;
6333
		return $index;
6334
	};
6335
6336
	// This recursive function turns the index array into a regular expression
6337
	$index_to_regex = function(&$index, $delim) use (&$strlen, &$index_to_regex)
6338
	{
6339
		static $depth = 0;
6340
		$depth++;
6341
6342
		// Absolute max length for a regex is 32768, but we might need wiggle room
6343
		$max_length = 30000;
6344
6345
		$regex = array();
6346
		$length = 0;
6347
6348
		foreach ($index as $key => $value)
6349
		{
6350
			$key_regex = preg_quote($key, $delim);
6351
			$new_key = $key;
6352
6353
			if (empty($value))
6354
				$sub_regex = '';
6355
			else
6356
			{
6357
				$sub_regex = $index_to_regex($value, $delim);
6358
6359
				if (count(array_keys($value)) == 1)
6360
				{
6361
					$new_key_array = explode('(?' . '>', $sub_regex);
6362
					$new_key .= $new_key_array[0];
6363
				}
6364
				else
6365
					$sub_regex = '(?' . '>' . $sub_regex . ')';
6366
			}
6367
6368
			if ($depth > 1)
6369
				$regex[$new_key] = $key_regex . $sub_regex;
6370
			else
6371
			{
6372
				if (($length += strlen($key_regex) + 1) < $max_length || empty($regex))
6373
				{
6374
					$regex[$new_key] = $key_regex . $sub_regex;
6375
					unset($index[$key]);
6376
				}
6377
				else
6378
					break;
6379
			}
6380
		}
6381
6382
		// Sort by key length and then alphabetically
6383
		uksort($regex, function($k1, $k2) use (&$strlen)
6384
		{
6385
			$l1 = $strlen($k1);
6386
			$l2 = $strlen($k2);
6387
6388
			if ($l1 == $l2)
6389
				return strcmp($k1, $k2) > 0 ? 1 : -1;
6390
			else
6391
				return $l1 > $l2 ? -1 : 1;
6392
		});
6393
6394
		$depth--;
6395
		return implode('|', $regex);
6396
	};
6397
6398
	// Now that the functions are defined, let's do this thing
6399
	$index = array();
6400
	$regex = '';
6401
6402
	foreach ($strings as $string)
6403
		$index = $add_string_to_index($string, $index);
6404
6405
	if ($returnArray === true)
6406
	{
6407
		$regex = array();
6408
		while (!empty($index))
6409
			$regex[] = '(?' . '>' . $index_to_regex($index, $delim) . ')';
6410
	}
6411
	else
6412
		$regex = '(?' . '>' . $index_to_regex($index, $delim) . ')';
6413
6414
	// Restore PHP's internal character encoding to whatever it was originally
6415
	if (!empty($current_encoding))
6416
		mb_internal_encoding($current_encoding);
6417
6418
	return $regex;
6419
}
6420
6421
/**
6422
 * Check if the passed url has an SSL certificate.
6423
 *
6424
 * Returns true if a cert was found & false if not.
6425
 *
6426
 * @param string $url to check, in $boardurl format (no trailing slash).
6427
 */
6428
function ssl_cert_found($url)
6429
{
6430
	// This check won't work without OpenSSL
6431
	if (!extension_loaded('openssl'))
6432
		return true;
6433
6434
	// First, strip the subfolder from the passed url, if any
6435
	$parsedurl = parse_url($url);
6436
	$url = 'ssl://' . $parsedurl['host'] . ':443';
6437
6438
	// Next, check the ssl stream context for certificate info
6439
	if (version_compare(PHP_VERSION, '5.6.0', '<'))
6440
		$ssloptions = array("capture_peer_cert" => true);
6441
	else
6442
		$ssloptions = array("capture_peer_cert" => true, "verify_peer" => true, "allow_self_signed" => true);
6443
6444
	$result = false;
6445
	$context = stream_context_create(array("ssl" => $ssloptions));
6446
	$stream = @stream_socket_client($url, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
6447
	if ($stream !== false)
6448
	{
6449
		$params = stream_context_get_params($stream);
6450
		$result = isset($params["options"]["ssl"]["peer_certificate"]) ? true : false;
6451
	}
6452
	return $result;
6453
}
6454
6455
/**
6456
 * Check if the passed url has a redirect to https:// by querying headers.
6457
 *
6458
 * Returns true if a redirect was found & false if not.
6459
 * Note that when force_ssl = 2, SMF issues its own redirect...  So if this
6460
 * returns true, it may be caused by SMF, not necessarily an .htaccess redirect.
6461
 *
6462
 * @param string $url to check, in $boardurl format (no trailing slash).
6463
 */
6464
function https_redirect_active($url)
6465
{
6466
	// Ask for the headers for the passed url, but via http...
6467
	// Need to add the trailing slash, or it puts it there & thinks there's a redirect when there isn't...
6468
	$url = str_ireplace('https://', 'http://', $url) . '/';
6469
	$headers = @get_headers($url);
6470
	if ($headers === false)
6471
		return false;
6472
6473
	// Now to see if it came back https...
6474
	// First check for a redirect status code in first row (301, 302, 307)
6475
	if (strstr($headers[0], '301') === false && strstr($headers[0], '302') === false && strstr($headers[0], '307') === false)
6476
		return false;
6477
6478
	// Search for the location entry to confirm https
6479
	$result = false;
6480
	foreach ($headers as $header)
6481
	{
6482
		if (stristr($header, 'Location: https://') !== false)
6483
		{
6484
			$result = true;
6485
			break;
6486
		}
6487
	}
6488
	return $result;
6489
}
6490
6491
/**
6492
 * Build query_wanna_see_board and query_see_board for a userid
6493
 *
6494
 * Returns array with keys query_wanna_see_board and query_see_board
6495
 *
6496
 * @param int $userid of the user
6497
 */
6498
function build_query_board($userid)
6499
{
6500
	global $user_info, $modSettings, $smcFunc, $db_prefix;
6501
6502
	$query_part = array();
6503
	$groups = array();
6504
	$is_admin = false;
6505
	$mod_cache;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $mod_cache seems to be never defined.
Loading history...
6506
	$ignoreboards;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $ignoreboards seems to be never defined.
Loading history...
6507
6508
	// If we come from cron, we can't have a $user_info.
6509
	if (isset($user_info['id']) && $user_info['id'] == $userid)
6510
	{
6511
		$groups = $user_info['groups'];
6512
		$is_admin = $user_info['is_admin'];
6513
		$mod_cache = !empty($user_info['mod_cache']) ? $user_info['mod_cache'] : null;
0 ignored issues
show
Unused Code introduced by
The assignment to $mod_cache is dead and can be removed.
Loading history...
6514
		$ignoreboards = !empty($user_info['ignoreboards']) ? $user_info['ignoreboards'] : null;
6515
	}
6516
	else
6517
	{
6518
		$request = $smcFunc['db_query']('', '
6519
			SELECT mem.ignore_boards, mem.id_group, mem.additional_groups, mem.id_post_group
6520
			FROM {db_prefix}members AS mem
6521
			WHERE mem.id_member = {int:id_member}
6522
			LIMIT 1',
6523
			array(
6524
				'id_member' => $userid,
6525
			)
6526
		);
6527
6528
		$row = $smcFunc['db_fetch_assoc']($request);
6529
6530
		if (empty($row['additional_groups']))
6531
			$groups = array($row['id_group'], $row['id_post_group']);
6532
		else
6533
			$groups = array_merge(
6534
				array($row['id_group'], $row['id_post_group']),
6535
				explode(',', $row['additional_groups'])
6536
			);
6537
6538
		// Because history has proven that it is possible for groups to go bad - clean up in case.
6539
		foreach ($groups as $k => $v)
6540
			$groups[$k] = (int) $v;
6541
6542
		$is_admin = in_array(1, $groups);
6543
6544
		$ignoreboards = !empty($row['ignore_boards']) && !empty($modSettings['allow_ignore_boards']) ? explode(',', $row['ignore_boards']) : array();
6545
6546
		// What boards are they the moderator of?
6547
		$boards_mod = array();
6548
6549
		$request = $smcFunc['db_query']('', '
6550
			SELECT id_board
6551
			FROM {db_prefix}moderators
6552
			WHERE id_member = {int:current_member}',
6553
			array(
6554
				'current_member' => $userid,
6555
			)
6556
		);
6557
		while ($row = $smcFunc['db_fetch_assoc']($request))
6558
			$boards_mod[] = $row['id_board'];
6559
		$smcFunc['db_free_result']($request);
6560
6561
		// Can any of the groups they're in moderate any of the boards?
6562
		$request = $smcFunc['db_query']('', '
6563
			SELECT id_board
6564
			FROM {db_prefix}moderator_groups
6565
			WHERE id_group IN({array_int:groups})',
6566
			array(
6567
				'groups' => $groups,
6568
			)
6569
		);
6570
		while ($row = $smcFunc['db_fetch_assoc']($request))
6571
			$boards_mod[] = $row['id_board'];
6572
		$smcFunc['db_free_result']($request);
6573
6574
		// Just in case we've got duplicates here...
6575
		$boards_mod = array_unique($boards_mod);
6576
6577
		$mod_cache['mq'] = empty($boards_mod) ? '0=1' : 'b.id_board IN (' . implode(',', $boards_mod) . ')';
0 ignored issues
show
Comprehensibility Best Practice introduced by
$mod_cache was never initialized. Although not strictly required by PHP, it is generally a good practice to add $mod_cache = array(); before regardless.
Loading history...
6578
	}
6579
6580
	// Just build this here, it makes it easier to change/use - administrators can see all boards.
6581
	if ($is_admin)
6582
		$query_part['query_see_board'] = '1=1';
6583
	// Otherwise just the groups in $user_info['groups'].
6584
	else
6585
	{
6586
		$query_part['query_see_board'] = '
6587
			EXISTS (
6588
				SELECT bpv.id_board
6589
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
6590
				WHERE bpv.id_group IN ('. implode(',', $groups) .')
6591
					AND bpv.deny = 0
6592
					AND bpv.id_board = b.id_board
6593
			)';
6594
6595
		if (!empty($modSettings['deny_boards_access']))
6596
			$query_part['query_see_board'] .= '
6597
			AND NOT EXISTS (
6598
				SELECT bpv.id_board
6599
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
6600
				WHERE bpv.id_group IN ( '. implode(',', $groups) .')
6601
					AND bpv.deny = 1
6602
					AND bpv.id_board = b.id_board
6603
			)';
6604
	}
6605
6606
	// Build the list of boards they WANT to see.
6607
	// This will take the place of query_see_boards in certain spots, so it better include the boards they can see also
6608
6609
	// If they aren't ignoring any boards then they want to see all the boards they can see
6610
	if (empty($ignoreboards))
6611
		$query_part['query_wanna_see_board'] = $query_part['query_see_board'];
6612
	// Ok I guess they don't want to see all the boards
6613
	else
6614
		$query_part['query_wanna_see_board'] = '(' . $query_part['query_see_board'] . ' AND b.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
6615
6616
	return $query_part;
6617
}
6618
6619
/**
6620
 * Check if the connection is using https.
6621
 *
6622
 * @return boolean true if connection used https
6623
 */
6624
function httpsOn()
6625
{
6626
	$secure = false;
6627
6628
	if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on')
6629
		$secure = true;
6630
	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...
6631
		$secure = true;
6632
6633
	return $secure;
6634
}
6635
6636
/**
6637
 * A wrapper for `filter_var($url, FILTER_VALIDATE_URL)` that can handle URLs
6638
 * with international characters (a.k.a. IRIs)
6639
 *
6640
 * @param string $iri The IRI to test.
6641
 * @param int $flags Optional flags to pass to filter_var()
6642
 * @return string|bool Either the original IRI, or false if the IRI was invalid.
6643
 */
6644
function validate_iri($iri, $flags = null)
6645
{
6646
	$url = iri_to_url($iri);
6647
6648
	if (filter_var($url, FILTER_VALIDATE_URL, $flags) !== false)
6649
		return $iri;
6650
	else
6651
		return false;
6652
}
6653
6654
/**
6655
 * A wrapper for `filter_var($url, FILTER_SANITIZE_URL)` that can handle URLs
6656
 * with international characters (a.k.a. IRIs)
6657
 *
6658
 * Note: The returned value will still be an IRI, not a URL. To convert to URL,
6659
 * feed the result of this function to iri_to_url()
6660
 *
6661
 * @param string $iri The IRI to sanitize.
6662
 * @return string|bool The sanitized version of the IRI
6663
 */
6664
function sanitize_iri($iri)
6665
{
6666
	// Encode any non-ASCII characters (but not space or control characters of any sort)
6667
	$iri = preg_replace_callback('~[^\x00-\x7F\pZ\pC]~u', function($matches)
6668
	{
6669
		return rawurlencode($matches[0]);
6670
	}, $iri);
6671
6672
	// Perform normal sanitization
6673
	$iri = filter_var($iri, FILTER_SANITIZE_URL);
6674
6675
	// Decode the non-ASCII characters
6676
	$iri = rawurldecode($iri);
6677
6678
	return $iri;
6679
}
6680
6681
/**
6682
 * Converts a URL with international characters (an IRI) into a pure ASCII URL
6683
 *
6684
 * Uses Punycode to encode any non-ASCII characters in the domain name, and uses
6685
 * standard URL encoding on the rest.
6686
 *
6687
 * @param string $iri A IRI that may or may not contain non-ASCII characters.
6688
 * @return string|bool The URL version of the IRI.
6689
 */
6690
function iri_to_url($iri)
6691
{
6692
	global $sourcedir;
6693
6694
	$host = parse_url((strpos($iri, '://') === false ? 'http://' : '') . ltrim($iri, ':/'), PHP_URL_HOST);
6695
6696
	if (empty($host))
6697
		return $iri;
6698
6699
	// Convert the domain using the Punycode algorithm
6700
	require_once($sourcedir . '/Class-Punycode.php');
6701
	$Punycode = new Punycode();
6702
	$encoded_host = $Punycode->encode($host);
6703
	$pos = strpos($iri, $host);
6704
	$iri = substr_replace($iri, $encoded_host, $pos, strlen($host));
6705
6706
	// Encode any disallowed characters in the rest of the URL
6707
	$unescaped = array(
6708
		'%21' => '!', '%23' => '#', '%24' => '$', '%26' => '&',
6709
		'%27' => "'", '%28' => '(', '%29' => ')', '%2A' => '*',
6710
		'%2B' => '+', '%2C' => ',', '%2F' => '/', '%3A' => ':',
6711
		'%3B' => ';', '%3D' => '=', '%3F' => '?', '%40' => '@',
6712
		'%25' => '%',
6713
	);
6714
	$iri = strtr(rawurlencode($iri), $unescaped);
6715
6716
	return $iri;
6717
}
6718
6719
/**
6720
 * Decodes a URL containing encoded international characters to UTF-8
6721
 *
6722
 * Decodes any Punycode encoded characters in the domain name, then uses
6723
 * standard URL decoding on the rest.
6724
 *
6725
 * @param string $url The pure ASCII version of a URL.
6726
 * @return string|bool The UTF-8 version of the URL.
6727
 */
6728
function url_to_iri($url)
6729
{
6730
	global $sourcedir;
6731
6732
	$host = parse_url((strpos($url, '://') === false ? 'http://' : '') . ltrim($url, ':/'), PHP_URL_HOST);
6733
6734
	if (empty($host))
6735
		return $url;
6736
6737
	// Decode the domain from Punycode
6738
	require_once($sourcedir . '/Class-Punycode.php');
6739
	$Punycode = new Punycode();
6740
	$decoded_host = $Punycode->decode($host);
6741
	$pos = strpos($url, $host);
6742
	$url = substr_replace($url, $decoded_host, $pos, strlen($host));
6743
6744
	// Decode the rest of the URL
6745
	$url = rawurldecode($url);
6746
6747
	return $url;
6748
}
6749
6750
/**
6751
 * Ensures SMF's scheduled tasks are being run as intended
6752
 *
6753
 * If the admin activated the cron_is_real_cron setting, but the cron job is
6754
 * not running things at least once per day, we need to go back to SMF's default
6755
 * behaviour using "web cron" JavaScript calls.
6756
 */
6757
function check_cron()
6758
{
6759
	global $user_info, $modSettings, $smcFunc, $txt;
6760
6761
	if (empty($modSettings['cron_last_checked']))
6762
		$modSettings['cron_last_checked'] = 0;
6763
6764
	if (!empty($modSettings['cron_is_real_cron']) && time() - $modSettings['cron_last_checked'] > 84600)
6765
	{
6766
		$request = $smcFunc['db_query']('', '
6767
			SELECT time_run
6768
			FROM {db_prefix}log_scheduled_tasks
6769
			ORDER BY id_log DESC
6770
			LIMIT 1',
6771
			array()
6772
		);
6773
		list($time_run) = $smcFunc['db_fetch_row']($request);
6774
		$smcFunc['db_free_result']($request);
6775
6776
		// If it's been more than 24 hours since the last task ran, cron must not be working
6777
		if (!empty($time_run) && time() - $time_run > 84600)
6778
		{
6779
			loadLanguage('ManageScheduledTasks');
6780
			log_error($txt['cron_not_working']);
6781
			updateSettings(array('cron_is_real_cron' => 0));
6782
		}
6783
		else
6784
			updateSettings(array('cron_last_checked' => time()));
6785
	}
6786
}
6787
6788
/**
6789
 * Sends an appropriate HTTP status header based on a given status code
6790
 *
6791
 * @param int $code The status code
6792
 * @param string $status The string for the status. Set automatically if not provided.
6793
 */
6794
function send_http_status($code, $status = '')
6795
{
6796
	$statuses = array(
6797
		206 => 'Partial Content',
6798
		304 => 'Not Modified',
6799
		400 => 'Bad Request',
6800
		403 => 'Forbidden',
6801
		404 => 'Not Found',
6802
		410 => 'Gone',
6803
		500 => 'Internal Server Error',
6804
		503 => 'Service Unavailable',
6805
	);
6806
6807
	$protocol = preg_match('~^\s*(HTTP/[12]\.\d)\s*$~i', $_SERVER['SERVER_PROTOCOL'], $matches) ? $matches[1] : 'HTTP/1.0';
6808
6809
	if (!isset($statuses[$code]) && empty($status))
6810
		header($protocol . ' 500 Internal Server Error');
6811
	else
6812
		header($protocol . ' ' . $code . ' ' . (!empty($status) ? $status : $statuses[$code]));
6813
}
6814
6815
/**
6816
 * Concatenates an array of strings into a grammatically correct sentence list
6817
 *
6818
 * Uses formats defined in the language files to build the list appropropriately
6819
 * for the currently loaded language.
6820
 *
6821
 * @param array $list An array of strings to concatenate.
6822
 * @return string The localized sentence list.
6823
 */
6824
function sentence_list($list)
6825
{
6826
	global $txt;
6827
6828
	// Make sure the bare necessities are defined
6829
	if (empty($txt['sentence_list_format']['n']))
6830
		$txt['sentence_list_format']['n'] = '{series}';
6831
	if (!isset($txt['sentence_list_separator']))
6832
		$txt['sentence_list_separator'] = ', ';
6833
	if (!isset($txt['sentence_list_separator_alt']))
6834
		$txt['sentence_list_separator_alt'] = '; ';
6835
6836
	// Which format should we use?
6837
	if (isset($txt['sentence_list_format'][count($list)]))
6838
		$format = $txt['sentence_list_format'][count($list)];
6839
	else
6840
		$format = $txt['sentence_list_format']['n'];
6841
6842
	// Do we want the normal separator or the alternate?
6843
	$separator = $txt['sentence_list_separator'];
6844
	foreach ($list as $item)
6845
	{
6846
		if (strpos($item, $separator) !== false)
6847
		{
6848
			$separator = $txt['sentence_list_separator_alt'];
6849
			$format = strtr($format, trim($txt['sentence_list_separator']), trim($separator));
6850
			break;
6851
		}
6852
	}
6853
6854
	$replacements = array();
6855
6856
	// Special handling for the last items on the list
6857
	$i = 0;
6858
	while (empty($done))
6859
	{
6860
		if (strpos($format, '{'. --$i . '}') !== false)
6861
			$replacements['{'. $i . '}'] = array_pop($list);
6862
		else
6863
			$done = true;
6864
	}
6865
	unset($done);
6866
6867
	// Special handling for the first items on the list
6868
	$i = 0;
6869
	while (empty($done))
6870
	{
6871
		if (strpos($format, '{'. ++$i . '}') !== false)
6872
			$replacements['{'. $i . '}'] = array_shift($list);
6873
		else
6874
			$done = true;
6875
	}
6876
	unset($done);
6877
6878
	// Whatever is left
6879
	$replacements['{series}'] = implode($separator, $list);
6880
6881
	// Do the deed
6882
	return strtr($format, $replacements);
6883
}
6884
6885
?>