Passed
Push — release-2.1 ( 072637...538fa9 )
by Jon
08:45 queued 03:49
created

sentence_list()   F

Complexity

Conditions 11
Paths 432

Size

Total Lines 59
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 34
c 0
b 0
f 0
nc 432
nop 1
dl 0
loc 59
rs 3.9388

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 2018 Simple Machines and individual contributors
11
 * @license http://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1 Beta 4
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, false) . ' ';
395
				$val = $val . ' END';
396
				$type = 'raw';
397
			}
398
			else
399
				$val = alert_count($members, false);
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="generic_icons previous_page"></span>',
595
			'current_page' => '<span class="current_page">%1$d</span> ',
596
			'page' => '<a class="navPages" 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="generic_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;
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
1083
		// The YouTube bbc needs this for its origin parameter
1084
		$scripturl_parts = parse_url($scripturl);
1085
		$hosturl = $scripturl_parts['scheme'] . '://' . $scripturl_parts['host'];
1086
1087
		/* The following bbc are formatted as an array, with keys as follows:
1088
1089
			tag: the tag's name - should be lowercase!
1090
1091
			type: one of...
1092
				- (missing): [tag]parsed content[/tag]
1093
				- unparsed_equals: [tag=xyz]parsed content[/tag]
1094
				- parsed_equals: [tag=parsed data]parsed content[/tag]
1095
				- unparsed_content: [tag]unparsed content[/tag]
1096
				- closed: [tag], [tag/], [tag /]
1097
				- unparsed_commas: [tag=1,2,3]parsed content[/tag]
1098
				- unparsed_commas_content: [tag=1,2,3]unparsed content[/tag]
1099
				- unparsed_equals_content: [tag=...]unparsed content[/tag]
1100
1101
			parameters: an optional array of parameters, for the form
1102
			  [tag abc=123]content[/tag].  The array is an associative array
1103
			  where the keys are the parameter names, and the values are an
1104
			  array which may contain the following:
1105
				- match: a regular expression to validate and match the value.
1106
				- quoted: true if the value should be quoted.
1107
				- validate: callback to evaluate on the data, which is $data.
1108
				- value: a string in which to replace $1 with the data.
1109
				  either it or validate may be used, not both.
1110
				- optional: true if the parameter is optional.
1111
1112
			test: a regular expression to test immediately after the tag's
1113
			  '=', ' ' or ']'.  Typically, should have a \] at the end.
1114
			  Optional.
1115
1116
			content: only available for unparsed_content, closed,
1117
			  unparsed_commas_content, and unparsed_equals_content.
1118
			  $1 is replaced with the content of the tag.  Parameters
1119
			  are replaced in the form {param}.  For unparsed_commas_content,
1120
			  $2, $3, ..., $n are replaced.
1121
1122
			before: only when content is not used, to go before any
1123
			  content.  For unparsed_equals, $1 is replaced with the value.
1124
			  For unparsed_commas, $1, $2, ..., $n are replaced.
1125
1126
			after: similar to before in every way, except that it is used
1127
			  when the tag is closed.
1128
1129
			disabled_content: used in place of content when the tag is
1130
			  disabled.  For closed, default is '', otherwise it is '$1' if
1131
			  block_level is false, '<div>$1</div>' elsewise.
1132
1133
			disabled_before: used in place of before when disabled.  Defaults
1134
			  to '<div>' if block_level, '' if not.
1135
1136
			disabled_after: used in place of after when disabled.  Defaults
1137
			  to '</div>' if block_level, '' if not.
1138
1139
			block_level: set to true the tag is a "block level" tag, similar
1140
			  to HTML.  Block level tags cannot be nested inside tags that are
1141
			  not block level, and will not be implicitly closed as easily.
1142
			  One break following a block level tag may also be removed.
1143
1144
			trim: if set, and 'inside' whitespace after the begin tag will be
1145
			  removed.  If set to 'outside', whitespace after the end tag will
1146
			  meet the same fate.
1147
1148
			validate: except when type is missing or 'closed', a callback to
1149
			  validate the data as $data.  Depending on the tag's type, $data
1150
			  may be a string or an array of strings (corresponding to the
1151
			  replacement.)
1152
1153
			quoted: when type is 'unparsed_equals' or 'parsed_equals' only,
1154
			  may be not set, 'optional', or 'required' corresponding to if
1155
			  the content may be quoted.  This allows the parser to read
1156
			  [tag="abc]def[esdf]"] properly.
1157
1158
			require_parents: an array of tag names, or not set.  If set, the
1159
			  enclosing tag *must* be one of the listed tags, or parsing won't
1160
			  occur.
1161
1162
			require_children: similar to require_parents, if set children
1163
			  won't be parsed if they are not in the list.
1164
1165
			disallow_children: similar to, but very different from,
1166
			  require_children, if it is set the listed tags will not be
1167
			  parsed inside the tag.
1168
1169
			parsed_tags_allowed: an array restricting what BBC can be in the
1170
			  parsed_equals parameter, if desired.
1171
		*/
1172
1173
		$codes = array(
1174
			array(
1175
				'tag' => 'abbr',
1176
				'type' => 'unparsed_equals',
1177
				'before' => '<abbr title="$1">',
1178
				'after' => '</abbr>',
1179
				'quoted' => 'optional',
1180
				'disabled_after' => ' ($1)',
1181
			),
1182
			array(
1183
				'tag' => 'acronym',
1184
				'type' => 'unparsed_equals',
1185
				'before' => '<acronym title="$1">',
1186
				'after' => '</acronym>',
1187
				'quoted' => 'optional',
1188
				'disabled_after' => ' ($1)',
1189
			),
1190
			array(
1191
				'tag' => 'anchor',
1192
				'type' => 'unparsed_equals',
1193
				'test' => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]',
1194
				'before' => '<span id="post_$1">',
1195
				'after' => '</span>',
1196
			),
1197
			array(
1198
				'tag' => 'attach',
1199
				'type' => 'unparsed_content',
1200
				'parameters' => array(
1201
					'name' => array('optional' => true),
1202
					'type' => array('optional' => true),
1203
					'alt' => array('optional' => true),
1204
					'title' => array('optional' => true),
1205
					'width' => array('optional' => true, 'match' => '(\d+)'),
1206
					'height' => array('optional' => true, 'match' => '(\d+)'),
1207
				),
1208
				'content' => '$1',
1209
				'validate' => function(&$tag, &$data, $disabled, $params) use ($modSettings, $context, $sourcedir, $txt)
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...
1210
				{
1211
					$returnContext = '';
1212
1213
					// BBC or the entire attachments feature is disabled
1214
					if (empty($modSettings['attachmentEnable']) || !empty($disabled['attach']))
1215
						return $data;
1216
1217
					// Save the attach ID.
1218
					$attachID = $data;
1219
1220
					// Kinda need this.
1221
					require_once($sourcedir . '/Subs-Attachments.php');
1222
1223
					$currentAttachment = parseAttachBBC($attachID);
1224
1225
					// parseAttachBBC will return a string ($txt key) rather than dying with a fatal_error. Up to you to decide what to do.
1226
					if (is_string($currentAttachment))
1227
						return $data = !empty($txt[$currentAttachment]) ? $txt[$currentAttachment] : $currentAttachment;
1228
1229
					if (!empty($currentAttachment['is_image']))
1230
					{
1231
						$alt = ' alt="' . (!empty($params['{alt}']) ? $params['{alt}'] : $currentAttachment['name']) . '"';
1232
						$title = !empty($params['{title}']) ? ' title="' . $params['{title}'] . '"' : '';
1233
1234
						$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"' : '';
1235
						$height = !empty($params['{height}']) ? ' height="' . $params['{height}'] . '"' : '';
1236
1237
						if (empty($width) && empty($height))
1238
						{
1239
							$width = ' width="' . $currentAttachment['width'] . '"';
1240
							$height = ' height="' . $currentAttachment['height'] . '"';
1241
						}
1242
1243
						if ($currentAttachment['thumbnail']['has_thumb'] && empty($params['{width}']) && empty($params['{height}']))
1244
							$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>';
1245
						else
1246
							$returnContext .= '<img src="' . $currentAttachment['href'] . ';image"' . $alt . $title . $width . $height . ' class="bbc_img"/>';
1247
					}
1248
1249
					// No image. Show a link.
1250
					else
1251
						$returnContext .= $currentAttachment['link'];
1252
1253
					// Gotta append what we just did.
1254
					$data = $returnContext;
1255
				},
1256
			),
1257
			array(
1258
				'tag' => 'b',
1259
				'before' => '<b>',
1260
				'after' => '</b>',
1261
			),
1262
			array(
1263
				'tag' => 'bdo',
1264
				'type' => 'unparsed_equals',
1265
				'before' => '<bdo dir="$1">',
1266
				'after' => '</bdo>',
1267
				'test' => '(rtl|ltr)\]',
1268
				'block_level' => true,
1269
			),
1270
			array(
1271
				'tag' => 'black',
1272
				'before' => '<span style="color: black;" class="bbc_color">',
1273
				'after' => '</span>',
1274
			),
1275
			array(
1276
				'tag' => 'blue',
1277
				'before' => '<span style="color: blue;" class="bbc_color">',
1278
				'after' => '</span>',
1279
			),
1280
			array(
1281
				'tag' => 'br',
1282
				'type' => 'closed',
1283
				'content' => '<br />',
1284
			),
1285
			array(
1286
				'tag' => 'center',
1287
				'before' => '<div class="centertext">',
1288
				'after' => '</div>',
1289
				'block_level' => true,
1290
			),
1291
			array(
1292
				'tag' => 'code',
1293
				'type' => 'unparsed_content',
1294
				'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>',
1295
				// @todo Maybe this can be simplified?
1296
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1297
				{
1298
					if (!isset($disabled['code']))
1299
					{
1300
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
1301
1302
						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

1302
						for ($php_i = 0, $php_n = count(/** @scrutinizer ignore-type */ $php_parts); $php_i < $php_n; $php_i++)
Loading history...
1303
						{
1304
							// Do PHP code coloring?
1305
							if ($php_parts[$php_i] != '&lt;?php')
1306
								continue;
1307
1308
							$php_string = '';
1309
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1310
							{
1311
								$php_string .= $php_parts[$php_i];
1312
								$php_parts[$php_i++] = '';
1313
							}
1314
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1315
						}
1316
1317
						// Fix the PHP code stuff...
1318
						$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

1318
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', /** @scrutinizer ignore-type */ $php_parts));
Loading history...
1319
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1320
1321
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1322
						if (!empty($context['browser']['is_opera']))
1323
							$data .= '&nbsp;';
1324
					}
1325
				},
1326
				'block_level' => true,
1327
			),
1328
			array(
1329
				'tag' => 'code',
1330
				'type' => 'unparsed_equals_content',
1331
				'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>',
1332
				// @todo Maybe this can be simplified?
1333
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1334
				{
1335
					if (!isset($disabled['code']))
1336
					{
1337
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1338
1339
						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

1339
						for ($php_i = 0, $php_n = count(/** @scrutinizer ignore-type */ $php_parts); $php_i < $php_n; $php_i++)
Loading history...
1340
						{
1341
							// Do PHP code coloring?
1342
							if ($php_parts[$php_i] != '&lt;?php')
1343
								continue;
1344
1345
							$php_string = '';
1346
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1347
							{
1348
								$php_string .= $php_parts[$php_i];
1349
								$php_parts[$php_i++] = '';
1350
							}
1351
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1352
						}
1353
1354
						// Fix the PHP code stuff...
1355
						$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

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

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

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

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

3173
					ob_start(/** @scrutinizer ignore-type */ $call);
Loading history...
3174
			}
3175
3176
		// Display the screen in the logical order.
3177
		template_header();
3178
		$header_done = true;
3179
	}
3180
	if ($do_footer)
3181
	{
3182
		loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main');
3183
3184
		// Anything special to put out?
3185
		if (!empty($context['insert_after_template']) && !isset($_REQUEST['xml']))
3186
			echo $context['insert_after_template'];
3187
3188
		// Just so we don't get caught in an endless loop of errors from the footer...
3189
		if (!$footer_done)
3190
		{
3191
			$footer_done = true;
3192
			template_footer();
3193
3194
			// (since this is just debugging... it's okay that it's after </html>.)
3195
			if (!isset($_REQUEST['xml']))
3196
				displayDebug();
3197
		}
3198
	}
3199
3200
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
3201
	if (strpos($_SERVER['REQUEST_URL'], 'action=dlattach') === false && strpos($_SERVER['REQUEST_URL'], 'action=viewsmfile') === false)
3202
		$_SESSION['old_url'] = $_SERVER['REQUEST_URL'];
3203
3204
	// For session check verification.... don't switch browsers...
3205
	$_SESSION['USER_AGENT'] = empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT'];
3206
3207
	// Hand off the output to the portal, etc. we're integrated with.
3208
	call_integration_hook('integrate_exit', array($do_footer));
3209
3210
	// Don't exit if we're coming from index.php; that will pass through normally.
3211
	if (!$from_index)
3212
		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...
3213
}
3214
3215
/**
3216
 * Get the size of a specified image with better error handling.
3217
 *
3218
 * @todo see if it's better in Subs-Graphics, but one step at the time.
3219
 * Uses getimagesize() to determine the size of a file.
3220
 * Attempts to connect to the server first so it won't time out.
3221
 *
3222
 * @param string $url The URL of the image
3223
 * @return array|false The image size as array (width, height), or false on failure
3224
 */
3225
function url_image_size($url)
3226
{
3227
	global $sourcedir;
3228
3229
	// Make sure it is a proper URL.
3230
	$url = str_replace(' ', '%20', $url);
3231
3232
	// Can we pull this from the cache... please please?
3233
	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...
3234
		return $temp;
3235
	$t = microtime(true);
3236
3237
	// Get the host to pester...
3238
	preg_match('~^\w+://(.+?)/(.*)$~', $url, $match);
3239
3240
	// Can't figure it out, just try the image size.
3241
	if ($url == '' || $url == 'http://' || $url == 'https://')
3242
	{
3243
		return false;
3244
	}
3245
	elseif (!isset($match[1]))
3246
	{
3247
		$size = @getimagesize($url);
3248
	}
3249
	else
3250
	{
3251
		// Try to connect to the server... give it half a second.
3252
		$temp = 0;
3253
		$fp = @fsockopen($match[1], 80, $temp, $temp, 0.5);
3254
3255
		// Successful?  Continue...
3256
		if ($fp != false)
3257
		{
3258
			// Send the HEAD request (since we don't have to worry about chunked, HTTP/1.1 is fine here.)
3259
			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");
3260
3261
			// Read in the HTTP/1.1 or whatever.
3262
			$test = substr(fgets($fp, 11), -1);
3263
			fclose($fp);
3264
3265
			// See if it returned a 404/403 or something.
3266
			if ($test < 4)
3267
			{
3268
				$size = @getimagesize($url);
3269
3270
				// This probably means allow_url_fopen is off, let's try GD.
3271
				if ($size === false && function_exists('imagecreatefromstring'))
3272
				{
3273
					// It's going to hate us for doing this, but another request...
3274
					$image = @imagecreatefromstring(fetch_web_data($url));
3275
					if ($image !== false)
3276
					{
3277
						$size = array(imagesx($image), imagesy($image));
3278
						imagedestroy($image);
3279
					}
3280
				}
3281
			}
3282
		}
3283
	}
3284
3285
	// If we didn't get it, we failed.
3286
	if (!isset($size))
3287
		$size = false;
3288
3289
	// If this took a long time, we may never have to do it again, but then again we might...
3290
	if (microtime(true) - $t > 0.8)
3291
		cache_put_data('url_image_size-' . md5($url), $size, 240);
3292
3293
	// Didn't work.
3294
	return $size;
3295
}
3296
3297
/**
3298
 * Sets up the basic theme context stuff.
3299
 *
3300
 * @param bool $forceload Whether to load the theme even if it's already loaded
3301
 */
3302
function setupThemeContext($forceload = false)
3303
{
3304
	global $modSettings, $user_info, $scripturl, $context, $settings, $options, $txt, $maintenance;
3305
	global $smcFunc;
3306
	static $loaded = false;
3307
3308
	// Under SSI this function can be called more then once.  That can cause some problems.
3309
	//   So only run the function once unless we are forced to run it again.
3310
	if ($loaded && !$forceload)
3311
		return;
3312
3313
	$loaded = true;
3314
3315
	$context['in_maintenance'] = !empty($maintenance);
3316
	$context['current_time'] = timeformat(time(), false);
3317
	$context['current_action'] = isset($_GET['action']) ? $smcFunc['htmlspecialchars']($_GET['action']) : '';
3318
3319
	// Get some news...
3320
	$context['news_lines'] = array_filter(explode("\n", str_replace("\r", '', trim(addslashes($modSettings['news'])))));
3321
	for ($i = 0, $n = count($context['news_lines']); $i < $n; $i++)
3322
	{
3323
		if (trim($context['news_lines'][$i]) == '')
3324
			continue;
3325
3326
		// Clean it up for presentation ;).
3327
		$context['news_lines'][$i] = parse_bbc(stripslashes(trim($context['news_lines'][$i])), true, 'news' . $i);
3328
	}
3329
	if (!empty($context['news_lines']))
3330
		$context['random_news_line'] = $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)];
3331
3332
	if (!$user_info['is_guest'])
3333
	{
3334
		$context['user']['messages'] = &$user_info['messages'];
3335
		$context['user']['unread_messages'] = &$user_info['unread_messages'];
3336
		$context['user']['alerts'] = &$user_info['alerts'];
3337
3338
		// Personal message popup...
3339
		if ($user_info['unread_messages'] > (isset($_SESSION['unread_messages']) ? $_SESSION['unread_messages'] : 0))
3340
			$context['user']['popup_messages'] = true;
3341
		else
3342
			$context['user']['popup_messages'] = false;
3343
		$_SESSION['unread_messages'] = $user_info['unread_messages'];
3344
3345
		if (allowedTo('moderate_forum'))
3346
			$context['unapproved_members'] = !empty($modSettings['unapprovedMembers']) ? $modSettings['unapprovedMembers'] : 0;
3347
3348
		$context['user']['avatar'] = array();
3349
3350
		// Check for gravatar first since we might be forcing them...
3351
		if (($modSettings['gravatarEnabled'] && substr($user_info['avatar']['url'], 0, 11) == 'gravatar://') || !empty($modSettings['gravatarOverride']))
3352
		{
3353
			if (!empty($modSettings['gravatarAllowExtraEmail']) && stristr($user_info['avatar']['url'], 'gravatar://') && strlen($user_info['avatar']['url']) > 11)
3354
				$context['user']['avatar']['href'] = get_gravatar_url($smcFunc['substr']($user_info['avatar']['url'], 11));
3355
			else
3356
				$context['user']['avatar']['href'] = get_gravatar_url($user_info['email']);
3357
		}
3358
		// Uploaded?
3359
		elseif ($user_info['avatar']['url'] == '' && !empty($user_info['avatar']['id_attach']))
3360
			$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';
3361
		// Full URL?
3362
		elseif (strpos($user_info['avatar']['url'], 'http://') === 0 || strpos($user_info['avatar']['url'], 'https://') === 0)
3363
			$context['user']['avatar']['href'] = $user_info['avatar']['url'];
3364
		// Otherwise we assume it's server stored.
3365
		elseif ($user_info['avatar']['url'] != '')
3366
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/' . $smcFunc['htmlspecialchars']($user_info['avatar']['url']);
3367
		// No avatar at all? Fine, we have a big fat default avatar ;)
3368
		else
3369
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/default.png';
3370
3371
		if (!empty($context['user']['avatar']))
3372
			$context['user']['avatar']['image'] = '<img src="' . $context['user']['avatar']['href'] . '" alt="" class="avatar">';
3373
3374
		// Figure out how long they've been logged in.
3375
		$context['user']['total_time_logged_in'] = array(
3376
			'days' => floor($user_info['total_time_logged_in'] / 86400),
3377
			'hours' => floor(($user_info['total_time_logged_in'] % 86400) / 3600),
3378
			'minutes' => floor(($user_info['total_time_logged_in'] % 3600) / 60)
3379
		);
3380
	}
3381
	else
3382
	{
3383
		$context['user']['messages'] = 0;
3384
		$context['user']['unread_messages'] = 0;
3385
		$context['user']['avatar'] = array();
3386
		$context['user']['total_time_logged_in'] = array('days' => 0, 'hours' => 0, 'minutes' => 0);
3387
		$context['user']['popup_messages'] = false;
3388
3389
		if (!empty($modSettings['registration_method']) && $modSettings['registration_method'] == 1)
3390
			$txt['welcome_guest'] .= $txt['welcome_guest_activate'];
3391
3392
		// If we've upgraded recently, go easy on the passwords.
3393
		if (!empty($modSettings['disableHashTime']) && ($modSettings['disableHashTime'] == 1 || time() < $modSettings['disableHashTime']))
3394
			$context['disable_login_hashing'] = true;
3395
	}
3396
3397
	// Setup the main menu items.
3398
	setupMenuContext();
3399
3400
	// This is here because old index templates might still use it.
3401
	$context['show_news'] = !empty($settings['enable_news']);
3402
3403
	// This is done to allow theme authors to customize it as they want.
3404
	$context['show_pm_popup'] = $context['user']['popup_messages'] && !empty($options['popup_messages']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != 'pm');
3405
3406
	// 2.1+: Add the PM popup here instead. Theme authors can still override it simply by editing/removing the 'fPmPopup' in the array.
3407
	if ($context['show_pm_popup'])
3408
		addInlineJavaScript('
3409
		jQuery(document).ready(function($) {
3410
			new smc_Popup({
3411
				heading: ' . JavaScriptEscape($txt['show_personal_messages_heading']) . ',
3412
				content: ' . JavaScriptEscape(sprintf($txt['show_personal_messages'], $context['user']['unread_messages'], $scripturl . '?action=pm')) . ',
3413
				icon_class: \'generic_icons mail_new\'
3414
			});
3415
		});');
3416
3417
	// Add a generic "Are you sure?" confirmation message.
3418
	addInlineJavaScript('
3419
	var smf_you_sure =' . JavaScriptEscape($txt['quickmod_confirm']) . ';');
3420
3421
	// Now add the capping code for avatars.
3422
	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')
3423
		addInlineCss('
3424
	img.avatar { max-width: ' . $modSettings['avatar_max_width_external'] . 'px; max-height: ' . $modSettings['avatar_max_height_external'] . 'px; }');
3425
3426
	// Add max image limits
3427
	if (!empty($modSettings['max_image_width']))
3428
		addInlineCss('
3429
	.postarea .bbc_img { max-width: ' . $modSettings['max_image_width'] . 'px; }');
3430
3431
	if (!empty($modSettings['max_image_height']))
3432
		addInlineCss('
3433
	.postarea .bbc_img { max-height: ' . $modSettings['max_image_height'] . 'px; }');
3434
3435
	// This looks weird, but it's because BoardIndex.php references the variable.
3436
	$context['common_stats']['latest_member'] = array(
3437
		'id' => $modSettings['latestMember'],
3438
		'name' => $modSettings['latestRealName'],
3439
		'href' => $scripturl . '?action=profile;u=' . $modSettings['latestMember'],
3440
		'link' => '<a href="' . $scripturl . '?action=profile;u=' . $modSettings['latestMember'] . '">' . $modSettings['latestRealName'] . '</a>',
3441
	);
3442
	$context['common_stats'] = array(
3443
		'total_posts' => comma_format($modSettings['totalMessages']),
3444
		'total_topics' => comma_format($modSettings['totalTopics']),
3445
		'total_members' => comma_format($modSettings['totalMembers']),
3446
		'latest_member' => $context['common_stats']['latest_member'],
3447
	);
3448
	$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']);
3449
3450
	if (empty($settings['theme_version']))
3451
		addJavaScriptVar('smf_scripturl', $scripturl);
3452
3453
	if (!isset($context['page_title']))
3454
		$context['page_title'] = '';
3455
3456
	// Set some specific vars.
3457
	$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3458
	$context['meta_keywords'] = !empty($modSettings['meta_keywords']) ? $smcFunc['htmlspecialchars']($modSettings['meta_keywords']) : '';
3459
3460
	// Content related meta tags, including Open Graph
3461
	$context['meta_tags'][] = array('property' => 'og:site_name', 'content' => $context['forum_name']);
3462
	$context['meta_tags'][] = array('property' => 'og:title', 'content' => $context['page_title_html_safe']);
3463
3464
	if (!empty($context['meta_keywords']))
3465
		$context['meta_tags'][] = array('name' => 'keywords', 'content' => $context['meta_keywords']);
3466
3467
	if (!empty($context['canonical_url']))
3468
		$context['meta_tags'][] = array('property' => 'og:url', 'content' => $context['canonical_url']);
3469
3470
	if (!empty($settings['og_image']))
3471
		$context['meta_tags'][] = array('property' => 'og:image', 'content' => $settings['og_image']);
3472
3473
	if (!empty($context['meta_description']))
3474
	{
3475
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['meta_description']);
3476
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['meta_description']);
3477
	}
3478
	else
3479
	{
3480
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['page_title_html_safe']);
3481
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['page_title_html_safe']);
3482
	}
3483
3484
	call_integration_hook('integrate_theme_context');
3485
}
3486
3487
/**
3488
 * Helper function to set the system memory to a needed value
3489
 * - If the needed memory is greater than current, will attempt to get more
3490
 * - if in_use is set to true, will also try to take the current memory usage in to account
3491
 *
3492
 * @param string $needed The amount of memory to request, if needed, like 256M
3493
 * @param bool $in_use Set to true to account for current memory usage of the script
3494
 * @return boolean True if we have at least the needed memory
3495
 */
3496
function setMemoryLimit($needed, $in_use = false)
3497
{
3498
	// everything in bytes
3499
	$memory_current = memoryReturnBytes(ini_get('memory_limit'));
3500
	$memory_needed = memoryReturnBytes($needed);
3501
3502
	// should we account for how much is currently being used?
3503
	if ($in_use)
3504
		$memory_needed += function_exists('memory_get_usage') ? memory_get_usage() : (2 * 1048576);
3505
3506
	// if more is needed, request it
3507
	if ($memory_current < $memory_needed)
3508
	{
3509
		@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

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

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

5203
			$fetch_data->get_url_data($url, /** @scrutinizer ignore-type */ $post_data);
Loading history...
5204
5205
			// no errors and a 200 result, then we have a good dataset, well we at least have data. ;)
5206
			if ($fetch_data->result('code') == 200 && !$fetch_data->result('error'))
5207
				$data = $fetch_data->result('body');
5208
			else
5209
				return false;
5210
		}
5211
5212
		// Neither fsockopen nor curl are available. Well, phooey.
5213
		else
5214
			return false;
5215
	}
5216
	else
5217
	{
5218
		// Umm, this shouldn't happen?
5219
		trigger_error('fetch_web_data(): Bad URL', E_USER_NOTICE);
5220
		$data = false;
5221
	}
5222
5223
	return $data;
5224
}
5225
5226
/**
5227
 * Prepares an array of "likes" info for the topic specified by $topic
5228
 *
5229
 * @param integer $topic The topic ID to fetch the info from.
5230
 * @return array An array of IDs of messages in the specified topic that the current user likes
5231
 */
5232
function prepareLikesContext($topic)
5233
{
5234
	global $user_info, $smcFunc;
5235
5236
	// Make sure we have something to work with.
5237
	if (empty($topic))
5238
		return array();
5239
5240
	// We already know the number of likes per message, we just want to know whether the current user liked it or not.
5241
	$user = $user_info['id'];
5242
	$cache_key = 'likes_topic_' . $topic . '_' . $user;
5243
	$ttl = 180;
5244
5245
	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...
5246
	{
5247
		$temp = array();
5248
		$request = $smcFunc['db_query']('', '
5249
			SELECT content_id
5250
			FROM {db_prefix}user_likes AS l
5251
				INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg)
5252
			WHERE l.id_member = {int:current_user}
5253
				AND l.content_type = {literal:msg}
5254
				AND m.id_topic = {int:topic}',
5255
			array(
5256
				'current_user' => $user,
5257
				'topic' => $topic,
5258
			)
5259
		);
5260
		while ($row = $smcFunc['db_fetch_assoc']($request))
5261
			$temp[] = (int) $row['content_id'];
5262
5263
		cache_put_data($cache_key, $temp, $ttl);
5264
	}
5265
5266
	return $temp;
5267
}
5268
5269
/**
5270
 * Microsoft uses their own character set Code Page 1252 (CP1252), which is a
5271
 * superset of ISO 8859-1, defining several characters between DEC 128 and 159
5272
 * that are not normally displayable.  This converts the popular ones that
5273
 * appear from a cut and paste from windows.
5274
 *
5275
 * @param string $string The string
5276
 * @return string The sanitized string
5277
 */
5278
function sanitizeMSCutPaste($string)
5279
{
5280
	global $context;
5281
5282
	if (empty($string))
5283
		return $string;
5284
5285
	// UTF-8 occurences of MS special characters
5286
	$findchars_utf8 = array(
5287
		"\xe2\x80\x9a",	// single low-9 quotation mark
5288
		"\xe2\x80\x9e",	// double low-9 quotation mark
5289
		"\xe2\x80\xa6",	// horizontal ellipsis
5290
		"\xe2\x80\x98",	// left single curly quote
5291
		"\xe2\x80\x99",	// right single curly quote
5292
		"\xe2\x80\x9c",	// left double curly quote
5293
		"\xe2\x80\x9d",	// right double curly quote
5294
		"\xe2\x80\x93",	// en dash
5295
		"\xe2\x80\x94",	// em dash
5296
	);
5297
5298
	// windows 1252 / iso equivalents
5299
	$findchars_iso = array(
5300
		chr(130),
5301
		chr(132),
5302
		chr(133),
5303
		chr(145),
5304
		chr(146),
5305
		chr(147),
5306
		chr(148),
5307
		chr(150),
5308
		chr(151),
5309
	);
5310
5311
	// safe replacements
5312
	$replacechars = array(
5313
		',',	// &sbquo;
5314
		',,',	// &bdquo;
5315
		'...',	// &hellip;
5316
		"'",	// &lsquo;
5317
		"'",	// &rsquo;
5318
		'"',	// &ldquo;
5319
		'"',	// &rdquo;
5320
		'-',	// &ndash;
5321
		'--',	// &mdash;
5322
	);
5323
5324
	if ($context['utf8'])
5325
		$string = str_replace($findchars_utf8, $replacechars, $string);
5326
	else
5327
		$string = str_replace($findchars_iso, $replacechars, $string);
5328
5329
	return $string;
5330
}
5331
5332
/**
5333
 * Decode numeric html entities to their ascii or UTF8 equivalent character.
5334
 *
5335
 * Callback function for preg_replace_callback in subs-members
5336
 * Uses capture group 2 in the supplied array
5337
 * Does basic scan to ensure characters are inside a valid range
5338
 *
5339
 * @param array $matches An array of matches (relevant info should be the 3rd item)
5340
 * @return string A fixed string
5341
 */
5342
function replaceEntities__callback($matches)
5343
{
5344
	global $context;
5345
5346
	if (!isset($matches[2]))
5347
		return '';
5348
5349
	$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

5349
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5350
5351
	// remove left to right / right to left overrides
5352
	if ($num === 0x202D || $num === 0x202E)
5353
		return '';
5354
5355
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
5356
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
5357
		return '&#' . $num . ';';
5358
5359
	if (empty($context['utf8']))
5360
	{
5361
		// no control characters
5362
		if ($num < 0x20)
5363
			return '';
5364
		// text is text
5365
		elseif ($num < 0x80)
5366
			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

5366
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5367
		// all others get html-ised
5368
		else
5369
			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

5369
			return '&#' . /** @scrutinizer ignore-type */ $matches[2] . ';';
Loading history...
5370
	}
5371
	else
5372
	{
5373
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
5374
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
5375
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
5376
			return '';
5377
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
5378
		elseif ($num < 0x80)
5379
			return chr($num);
5380
		// <0x800 (2048)
5381
		elseif ($num < 0x800)
5382
			return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5383
		// < 0x10000 (65536)
5384
		elseif ($num < 0x10000)
5385
			return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5386
		// <= 0x10FFFF (1114111)
5387
		else
5388
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5389
	}
5390
}
5391
5392
/**
5393
 * Converts html entities to utf8 equivalents
5394
 *
5395
 * Callback function for preg_replace_callback
5396
 * Uses capture group 1 in the supplied array
5397
 * Does basic checks to keep characters inside a viewable range.
5398
 *
5399
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
5400
 * @return string The fixed string
5401
 */
5402
function fixchar__callback($matches)
5403
{
5404
	if (!isset($matches[1]))
5405
		return '';
5406
5407
	$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

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

5415
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5416
	// <0x800 (2048)
5417
	elseif ($num < 0x800)
5418
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5419
	// < 0x10000 (65536)
5420
	elseif ($num < 0x10000)
5421
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5422
	// <= 0x10FFFF (1114111)
5423
	else
5424
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5425
}
5426
5427
/**
5428
 * Strips out invalid html entities, replaces others with html style &#123; codes
5429
 *
5430
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
5431
 * strpos, strlen, substr etc
5432
 *
5433
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
5434
 * @return string The fixed string
5435
 */
5436
function entity_fix__callback($matches)
5437
{
5438
	if (!isset($matches[2]))
5439
		return '';
5440
5441
	$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

5441
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5442
5443
	// we don't allow control characters, characters out of range, byte markers, etc
5444
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
5445
		return '';
5446
	else
5447
		return '&#' . $num . ';';
5448
}
5449
5450
/**
5451
 * Return a Gravatar URL based on
5452
 * - the supplied email address,
5453
 * - the global maximum rating,
5454
 * - the global default fallback,
5455
 * - maximum sizes as set in the admin panel.
5456
 *
5457
 * It is SSL aware, and caches most of the parameters.
5458
 *
5459
 * @param string $email_address The user's email address
5460
 * @return string The gravatar URL
5461
 */
5462
function get_gravatar_url($email_address)
5463
{
5464
	global $modSettings, $smcFunc;
5465
	static $url_params = null;
5466
5467
	if ($url_params === null)
5468
	{
5469
		$ratings = array('G', 'PG', 'R', 'X');
5470
		$defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank');
5471
		$url_params = array();
5472
		if (!empty($modSettings['gravatarMaxRating']) && in_array($modSettings['gravatarMaxRating'], $ratings))
5473
			$url_params[] = 'rating=' . $modSettings['gravatarMaxRating'];
5474
		if (!empty($modSettings['gravatarDefault']) && in_array($modSettings['gravatarDefault'], $defaults))
5475
			$url_params[] = 'default=' . $modSettings['gravatarDefault'];
5476
		if (!empty($modSettings['avatar_max_width_external']))
5477
			$size_string = (int) $modSettings['avatar_max_width_external'];
5478
		if (!empty($modSettings['avatar_max_height_external']) && !empty($size_string))
5479
			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...
5480
				$size_string = $modSettings['avatar_max_height_external'];
5481
5482
		if (!empty($size_string))
5483
			$url_params[] = 's=' . $size_string;
5484
	}
5485
	$http_method = !empty($modSettings['force_ssl']) ? 'https://secure' : 'http://www';
5486
5487
	return $http_method . '.gravatar.com/avatar/' . md5($smcFunc['strtolower']($email_address)) . '?' . implode('&', $url_params);
5488
}
5489
5490
/**
5491
 * Get a list of timezones.
5492
 *
5493
 * @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'.
5494
 * @return array An array of timezone info.
5495
 */
5496
function smf_list_timezones($when = 'now')
5497
{
5498
	global $smcFunc, $modSettings, $tztxt, $txt;
5499
	static $timezones = null, $lastwhen = null;
5500
5501
	// No point doing this over if we already did it once
5502
	if (!empty($timezones) && $when == $lastwhen)
5503
		return $timezones;
5504
	else
5505
		$lastwhen = $when;
5506
5507
	// Parseable datetime string?
5508
	if (is_int($timestamp = strtotime($when)))
0 ignored issues
show
introduced by
The condition is_int($timestamp = strtotime($when)) is always true.
Loading history...
5509
		$when = $timestamp;
5510
5511
	// A Unix timestamp?
5512
	elseif (is_numeric($when))
5513
		$when = intval($when);
5514
5515
	// Invalid value? Just get current Unix timestamp.
5516
	else
5517
		$when = time();
5518
5519
	// We'll need these too
5520
	$date_when = date_create('@' . $when);
5521
	$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

5521
	$later = (int) date_format(date_add(/** @scrutinizer ignore-type */ $date_when, date_interval_create_from_date_string('1 year')), 'U');
Loading history...
5522
5523
	// Load up any custom time zone descriptions we might have
5524
	loadLanguage('Timezones');
5525
5526
	// Should we put time zones from certain countries at the top of the list?
5527
	$priority_countries = !empty($modSettings['timezone_priority_countries']) ? explode(',', $modSettings['timezone_priority_countries']) : array();
5528
	$priority_tzids = array();
5529
	foreach ($priority_countries as $country)
5530
	{
5531
		$country_tzids = @timezone_identifiers_list(DateTimeZone::PER_COUNTRY, strtoupper(trim($country)));
5532
		if (!empty($country_tzids))
5533
			$priority_tzids = array_merge($priority_tzids, $country_tzids);
5534
	}
5535
5536
	// Antarctic research stations should be listed last, unless you're running a penguin forum
5537
	$low_priority_tzids = !in_array('AQ', $priority_countries) ? timezone_identifiers_list(DateTimeZone::ANTARCTICA) : array();
5538
5539
	// Process the preferred timezones first, then the normal ones, then the low priority ones.
5540
	$tzids = array_merge(array_keys($tztxt), array_diff(timezone_identifiers_list(), array_keys($tztxt), $low_priority_tzids), $low_priority_tzids);
0 ignored issues
show
Bug introduced by
It seems like $low_priority_tzids can also be of type false; however, parameter $_ of array_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

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

5540
	$tzids = array_merge(array_keys($tztxt), array_diff(timezone_identifiers_list(), array_keys($tztxt), $low_priority_tzids), /** @scrutinizer ignore-type */ $low_priority_tzids);
Loading history...
Bug introduced by
It seems like 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

5540
	$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...
5541
5542
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
5543
	foreach ($tzids as $tzid)
5544
	{
5545
		// We don't want UTC right now
5546
		if ($tzid == 'UTC')
5547
			continue;
5548
5549
		$tz = timezone_open($tzid);
5550
5551
		// First, get the set of transition rules for this tzid
5552
		$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

5552
		$tzinfo = timezone_transitions_get(/** @scrutinizer ignore-type */ $tz, $when, $later);
Loading history...
5553
5554
		// Use the entire set of transition rules as the array *key* so we can avoid duplicates
5555
		$tzkey = serialize($tzinfo);
5556
5557
		// Next, get the geographic info for this tzid
5558
		$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

5558
		$tzgeo = timezone_location_get(/** @scrutinizer ignore-type */ $tz);
Loading history...
5559
5560
		// Don't overwrite our preferred tzids
5561
		if (empty($zones[$tzkey]['tzid']))
5562
		{
5563
			$zones[$tzkey]['tzid'] = $tzid;
5564
			$zones[$tzkey]['abbr'] = $tzinfo[0]['abbr'];
5565
		}
5566
5567
		// A time zone from a prioritized country?
5568
		if (in_array($tzid, $priority_tzids))
5569
			$priority_zones[$tzkey] = true;
5570
5571
		// Keep track of the location and offset for this tzid
5572
		if (!empty($txt[$tzid]))
5573
			$zones[$tzkey]['locations'][] = $txt[$tzid];
5574
		else
5575
		{
5576
			$tzid_parts = explode('/', $tzid);
5577
			$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
5578
		}
5579
		$offsets[$tzkey] = $tzinfo[0]['offset'];
5580
		$longitudes[$tzkey] = empty($longitudes[$tzkey]) ? $tzgeo['longitude'] : $longitudes[$tzkey];
5581
	}
5582
5583
	// Sort by offset then longitude
5584
	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...
5585
5586
	// Build the final array of formatted values
5587
	$priority_timezones = array();
5588
	$timezones = array();
5589
	foreach ($zones as $tzkey => $tzvalue)
5590
	{
5591
		date_timezone_set($date_when, timezone_open($tzvalue['tzid']));
0 ignored issues
show
Bug introduced by
It seems like timezone_open($tzvalue['tzid']) can also be of type false; however, parameter $timezone of date_timezone_set() does only seem to accept DateTimeZone, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

5591
		date_timezone_set(/** @scrutinizer ignore-type */ $date_when, timezone_open($tzvalue['tzid']));
Loading history...
5592
5593
		// Use the custom description, if there is one
5594
		if (!empty($tztxt[$tzvalue['tzid']]))
5595
			$desc = $tztxt[$tzvalue['tzid']];
5596
		// Otherwise, use the list of locations (max 5, so things don't get silly)
5597
		else
5598
			$desc = implode(', ', array_slice(array_unique($tzvalue['locations']), 0, 5)) . (count($tzvalue['locations']) > 5 ? ', ' . $txt['etc'] : '');
5599
5600
		// Show the UTC offset and the abbreviation, if it's something like 'MST' and not '-06'
5601
		$desc = '[UTC' . date_format($date_when, 'P') . '] - ' . (!strspn($tzvalue['abbr'], '+-') ? $tzvalue['abbr'] . ' - ' : '') . $desc;
5602
5603
		if (isset($priority_zones[$tzkey]))
5604
			$priority_timezones[$tzvalue['tzid']] = $desc;
5605
		else
5606
			$timezones[$tzvalue['tzid']] = $desc;
5607
	}
5608
5609
	if (!empty($priority_timezones))
5610
		$priority_timezones[] = '-----';
5611
5612
	$timezones = array_merge(
5613
		$priority_timezones,
5614
		array('' => '(Forum Default)', 'UTC' => 'UTC - ' . $tztxt['UTC'], '-----'),
5615
		$timezones
5616
	);
5617
5618
	return $timezones;
5619
}
5620
5621
/**
5622
 * @param string $ip_address An IP address in IPv4, IPv6 or decimal notation
5623
 * @return string|false The IP address in binary or false
5624
 */
5625
function inet_ptod($ip_address)
5626
{
5627
	if (!isValidIP($ip_address))
5628
		return $ip_address;
5629
5630
	$bin = inet_pton($ip_address);
5631
	return $bin;
5632
}
5633
5634
/**
5635
 * @param string $bin An IP address in IPv4, IPv6 (Either string (postgresql) or binary (other databases))
5636
 * @return string|false The IP address in presentation format or false on error
5637
 */
5638
function inet_dtop($bin)
5639
{
5640
	if (empty($bin))
5641
		return '';
5642
5643
	global $db_type;
5644
5645
	if ($db_type == 'postgresql')
5646
		return $bin;
5647
5648
	$ip_address = inet_ntop($bin);
5649
5650
	return $ip_address;
5651
}
5652
5653
/**
5654
 * Safe serialize() and unserialize() replacements
5655
 *
5656
 * @license Public Domain
5657
 *
5658
 * @author anthon (dot) pang (at) gmail (dot) com
5659
 */
5660
5661
/**
5662
 * Safe serialize() replacement. Recursive
5663
 * - output a strict subset of PHP's native serialized representation
5664
 * - does not serialize objects
5665
 *
5666
 * @param mixed $value
5667
 * @return string
5668
 */
5669
function _safe_serialize($value)
5670
{
5671
	if (is_null($value))
5672
		return 'N;';
5673
5674
	if (is_bool($value))
5675
		return 'b:' . (int) $value . ';';
5676
5677
	if (is_int($value))
5678
		return 'i:' . $value . ';';
5679
5680
	if (is_float($value))
5681
		return 'd:' . str_replace(',', '.', $value) . ';';
5682
5683
	if (is_string($value))
5684
		return 's:' . strlen($value) . ':"' . $value . '";';
5685
5686
	if (is_array($value))
5687
	{
5688
		$out = '';
5689
		foreach ($value as $k => $v)
5690
			$out .= _safe_serialize($k) . _safe_serialize($v);
5691
5692
		return 'a:' . count($value) . ':{' . $out . '}';
5693
	}
5694
5695
	// safe_serialize cannot serialize resources or objects.
5696
	return false;
5697
}
5698
5699
/**
5700
 * Wrapper for _safe_serialize() that handles exceptions and multibyte encoding issues.
5701
 *
5702
 * @param mixed $value
5703
 * @return string
5704
 */
5705
function safe_serialize($value)
5706
{
5707
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
5708
	if (function_exists('mb_internal_encoding') &&
5709
		(((int) ini_get('mbstring.func_overload')) & 2))
5710
	{
5711
		$mbIntEnc = mb_internal_encoding();
5712
		mb_internal_encoding('ASCII');
5713
	}
5714
5715
	$out = _safe_serialize($value);
5716
5717
	if (isset($mbIntEnc))
5718
		mb_internal_encoding($mbIntEnc);
5719
5720
	return $out;
5721
}
5722
5723
/**
5724
 * Safe unserialize() replacement
5725
 * - accepts a strict subset of PHP's native serialized representation
5726
 * - does not unserialize objects
5727
 *
5728
 * @param string $str
5729
 * @return mixed
5730
 * @throw Exception if $str is malformed or contains unsupported types (e.g., resources, objects)
5731
 */
5732
function _safe_unserialize($str)
5733
{
5734
	// Input  is not a string.
5735
	if (empty($str) || !is_string($str))
5736
		return false;
5737
5738
	$stack = array();
5739
	$expected = array();
5740
5741
	/*
5742
	 * states:
5743
	 *   0 - initial state, expecting a single value or array
5744
	 *   1 - terminal state
5745
	 *   2 - in array, expecting end of array or a key
5746
	 *   3 - in array, expecting value or another array
5747
	 */
5748
	$state = 0;
5749
	while ($state != 1)
5750
	{
5751
		$type = isset($str[0]) ? $str[0] : '';
5752
		if ($type == '}')
5753
			$str = substr($str, 1);
5754
5755
		elseif ($type == 'N' && $str[1] == ';')
5756
		{
5757
			$value = null;
5758
			$str = substr($str, 2);
5759
		}
5760
		elseif ($type == 'b' && preg_match('/^b:([01]);/', $str, $matches))
5761
		{
5762
			$value = $matches[1] == '1' ? true : false;
5763
			$str = substr($str, 4);
5764
		}
5765
		elseif ($type == 'i' && preg_match('/^i:(-?[0-9]+);(.*)/s', $str, $matches))
5766
		{
5767
			$value = (int) $matches[1];
5768
			$str = $matches[2];
5769
		}
5770
		elseif ($type == 'd' && preg_match('/^d:(-?[0-9]+\.?[0-9]*(E[+-][0-9]+)?);(.*)/s', $str, $matches))
5771
		{
5772
			$value = (float) $matches[1];
5773
			$str = $matches[3];
5774
		}
5775
		elseif ($type == 's' && preg_match('/^s:([0-9]+):"(.*)/s', $str, $matches) && substr($matches[2], (int) $matches[1], 2) == '";')
5776
		{
5777
			$value = substr($matches[2], 0, (int) $matches[1]);
5778
			$str = substr($matches[2], (int) $matches[1] + 2);
5779
		}
5780
		elseif ($type == 'a' && preg_match('/^a:([0-9]+):{(.*)/s', $str, $matches))
5781
		{
5782
			$expectedLength = (int) $matches[1];
5783
			$str = $matches[2];
5784
		}
5785
5786
		// Object or unknown/malformed type.
5787
		else
5788
			return false;
5789
5790
		switch ($state)
5791
		{
5792
			case 3: // In array, expecting value or another array.
5793
				if ($type == 'a')
5794
				{
5795
					$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...
5796
					$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...
5797
					$list = &$list[$key];
5798
					$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...
5799
					$state = 2;
5800
					break;
5801
				}
5802
				if ($type != '}')
5803
				{
5804
					$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...
5805
					$state = 2;
5806
					break;
5807
				}
5808
5809
				// Missing array value.
5810
				return false;
5811
5812
			case 2: // in array, expecting end of array or a key
5813
				if ($type == '}')
5814
				{
5815
					// Array size is less than expected.
5816
					if (count($list) < end($expected))
5817
						return false;
5818
5819
					unset($list);
5820
					$list = &$stack[count($stack) - 1];
5821
					array_pop($stack);
5822
5823
					// Go to terminal state if we're at the end of the root array.
5824
					array_pop($expected);
5825
5826
					if (count($expected) == 0)
5827
						$state = 1;
5828
5829
					break;
5830
				}
5831
5832
				if ($type == 'i' || $type == 's')
5833
				{
5834
					// Array size exceeds expected length.
5835
					if (count($list) >= end($expected))
5836
						return false;
5837
5838
					$key = $value;
5839
					$state = 3;
5840
					break;
5841
				}
5842
5843
				// Illegal array index type.
5844
				return false;
5845
5846
			// Expecting array or value.
5847
			case 0:
5848
				if ($type == 'a')
5849
				{
5850
					$data = array();
5851
					$list = &$data;
5852
					$expected[] = $expectedLength;
5853
					$state = 2;
5854
					break;
5855
				}
5856
5857
				if ($type != '}')
5858
				{
5859
					$data = $value;
5860
					$state = 1;
5861
					break;
5862
				}
5863
5864
				// Not in array.
5865
				return false;
5866
		}
5867
	}
5868
5869
	// Trailing data in input.
5870
	if (!empty($str))
5871
		return false;
5872
5873
	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...
5874
}
5875
5876
/**
5877
 * Wrapper for _safe_unserialize() that handles exceptions and multibyte encoding issue
5878
 *
5879
 * @param string $str
5880
 * @return mixed
5881
 */
5882
function safe_unserialize($str)
5883
{
5884
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
5885
	if (function_exists('mb_internal_encoding') &&
5886
		(((int) ini_get('mbstring.func_overload')) & 0x02))
5887
	{
5888
		$mbIntEnc = mb_internal_encoding();
5889
		mb_internal_encoding('ASCII');
5890
	}
5891
5892
	$out = _safe_unserialize($str);
5893
5894
	if (isset($mbIntEnc))
5895
		mb_internal_encoding($mbIntEnc);
5896
5897
	return $out;
5898
}
5899
5900
/**
5901
 * Tries different modes to make file/dirs writable. Wrapper function for chmod()
5902
 *
5903
 * @param string $file The file/dir full path.
5904
 * @param int $value Not needed, added for legacy reasons.
5905
 * @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.
5906
 */
5907
function smf_chmod($file, $value = 0)
5908
{
5909
	// No file? no checks!
5910
	if (empty($file))
5911
		return false;
5912
5913
	// Already writable?
5914
	if (is_writable($file))
5915
		return true;
5916
5917
	// Do we have a file or a dir?
5918
	$isDir = is_dir($file);
5919
	$isWritable = false;
5920
5921
	// Set different modes.
5922
	$chmodValues = $isDir ? array(0750, 0755, 0775, 0777) : array(0644, 0664, 0666);
5923
5924
	foreach ($chmodValues as $val)
5925
	{
5926
		// If it's writable, break out of the loop.
5927
		if (is_writable($file))
5928
		{
5929
			$isWritable = true;
5930
			break;
5931
		}
5932
5933
		else
5934
			@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

5934
			/** @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...
5935
	}
5936
5937
	return $isWritable;
5938
}
5939
5940
/**
5941
 * Wrapper function for json_decode() with error handling.
5942
 *
5943
 * @param string $json The string to decode.
5944
 * @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.
5945
 * @param bool $logIt To specify if the error will be logged if theres any.
5946
 * @return array Either an empty array or the decoded data as an array.
5947
 */
5948
function smf_json_decode($json, $returnAsArray = false, $logIt = true)
5949
{
5950
	global $txt;
5951
5952
	// Come on...
5953
	if (empty($json) || !is_string($json))
5954
		return array();
5955
5956
	$returnArray = @json_decode($json, $returnAsArray);
5957
5958
	// PHP 5.3 so no json_last_error_msg()
5959
	switch (json_last_error())
5960
	{
5961
		case JSON_ERROR_NONE:
5962
			$jsonError = false;
5963
			break;
5964
		case JSON_ERROR_DEPTH:
5965
			$jsonError = 'JSON_ERROR_DEPTH';
5966
			break;
5967
		case JSON_ERROR_STATE_MISMATCH:
5968
			$jsonError = 'JSON_ERROR_STATE_MISMATCH';
5969
			break;
5970
		case JSON_ERROR_CTRL_CHAR:
5971
			$jsonError = 'JSON_ERROR_CTRL_CHAR';
5972
			break;
5973
		case JSON_ERROR_SYNTAX:
5974
			$jsonError = 'JSON_ERROR_SYNTAX';
5975
			break;
5976
		case JSON_ERROR_UTF8:
5977
			$jsonError = 'JSON_ERROR_UTF8';
5978
			break;
5979
		default:
5980
			$jsonError = 'unknown';
5981
			break;
5982
	}
5983
5984
	// Something went wrong!
5985
	if (!empty($jsonError) && $logIt)
5986
	{
5987
		// Being a wrapper means we lost our smf_error_handler() privileges :(
5988
		$jsonDebug = debug_backtrace();
5989
		$jsonDebug = $jsonDebug[0];
5990
		loadLanguage('Errors');
5991
5992
		if (!empty($jsonDebug))
5993
			log_error($txt['json_' . $jsonError], 'critical', $jsonDebug['file'], $jsonDebug['line']);
5994
5995
		else
5996
			log_error($txt['json_' . $jsonError], 'critical');
5997
5998
		// Everyone expects an array.
5999
		return array();
6000
	}
6001
6002
	return $returnArray;
6003
}
6004
6005
/**
6006
 * Check the given String if he is a valid IPv4 or IPv6
6007
 * return true or false
6008
 *
6009
 * @param string $IPString
6010
 *
6011
 * @return bool
6012
 */
6013
function isValidIP($IPString)
6014
{
6015
	return filter_var($IPString, FILTER_VALIDATE_IP) !== false;
6016
}
6017
6018
/**
6019
 * Outputs a response.
6020
 * It assumes the data is already a string.
6021
 *
6022
 * @param string $data The data to print
6023
 * @param string $type The content type. Defaults to Json.
6024
 * @return void
6025
 */
6026
function smf_serverResponse($data = '', $type = 'content-type: application/json')
6027
{
6028
	global $db_show_debug, $modSettings;
6029
6030
	// Defensive programming anyone?
6031
	if (empty($data))
6032
		return false;
6033
6034
	// Don't need extra stuff...
6035
	$db_show_debug = false;
6036
6037
	// Kill anything else.
6038
	ob_end_clean();
6039
6040
	if (!empty($modSettings['CompressedOutput']))
6041
		@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

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