Passed
Push — release-2.1 ( 0c2197...207d2d )
by Jeremy
05:47
created

smf_list_timezones()   F

Complexity

Conditions 21
Paths 16849

Size

Total Lines 124
Code Lines 63

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 21
eloc 63
c 0
b 0
f 0
nop 1
dl 0
loc 124
rs 0
nc 16849

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-View.php');
390
			if (is_array($members))
391
			{
392
				$val = 'CASE ';
393
				foreach ($members as $k => $v)
394
					$val .= 'WHEN id_member = ' . $v . ' THEN '. count(fetch_alerts($v, false, 0, array(), false)) . ' ';
395
				$val = $val . ' END';
396
				$type = 'raw';
397
			}
398
			else
399
			{
400
				$blub = fetch_alerts($members, false, 0, array(), false);
401
				$val = count($blub);
402
			}
403
		}
404
		else if ($type == 'int' && ($val === '+' || $val === '-'))
405
		{
406
			$val = $var . ' ' . $val . ' 1';
407
			$type = 'raw';
408
		}
409
410
		// Ensure posts, instant_messages, and unread_messages don't overflow or underflow.
411
		if (in_array($var, array('posts', 'instant_messages', 'unread_messages')))
412
		{
413
			if (preg_match('~^' . $var . ' (\+ |- |\+ -)([\d]+)~', $val, $match))
414
			{
415
				if ($match[1] != '+ ')
416
					$val = 'CASE WHEN ' . $var . ' <= ' . abs($match[2]) . ' THEN 0 ELSE ' . $val . ' END';
417
				$type = 'raw';
418
			}
419
		}
420
421
		$setString .= ' ' . $var . ' = {' . $type . ':p_' . $var . '},';
422
		$parameters['p_' . $var] = $val;
423
	}
424
425
	$smcFunc['db_query']('', '
426
		UPDATE {db_prefix}members
427
		SET' . substr($setString, 0, -1) . '
428
		WHERE ' . $condition,
429
		$parameters
430
	);
431
432
	updateStats('postgroups', $members, array_keys($data));
433
434
	// Clear any caching?
435
	if (!empty($modSettings['cache_enable']) && $modSettings['cache_enable'] >= 2 && !empty($members))
436
	{
437
		if (!is_array($members))
438
			$members = array($members);
439
440
		foreach ($members as $member)
441
		{
442
			if ($modSettings['cache_enable'] >= 3)
443
			{
444
				cache_put_data('member_data-profile-' . $member, null, 120);
445
				cache_put_data('member_data-normal-' . $member, null, 120);
446
				cache_put_data('member_data-minimal-' . $member, null, 120);
447
			}
448
			cache_put_data('user_settings-' . $member, null, 60);
449
		}
450
	}
451
}
452
453
/**
454
 * Updates the settings table as well as $modSettings... only does one at a time if $update is true.
455
 *
456
 * - updates both the settings table and $modSettings array.
457
 * - all of changeArray's indexes and values are assumed to have escaped apostrophes (')!
458
 * - if a variable is already set to what you want to change it to, that
459
 *   variable will be skipped over; it would be unnecessary to reset.
460
 * - When use_update is true, UPDATEs will be used instead of REPLACE.
461
 * - when use_update is true, the value can be true or false to increment
462
 *  or decrement it, respectively.
463
 *
464
 * @param array $changeArray An array of info about what we're changing in 'setting' => 'value' format
465
 * @param bool $update Whether to use an UPDATE query instead of a REPLACE query
466
 */
467
function updateSettings($changeArray, $update = false)
468
{
469
	global $modSettings, $smcFunc;
470
471
	if (empty($changeArray) || !is_array($changeArray))
472
		return;
473
474
	$toRemove = array();
475
476
	// Go check if there is any setting to be removed.
477
	foreach ($changeArray as $k => $v)
478
		if ($v === null)
479
		{
480
			// Found some, remove them from the original array and add them to ours.
481
			unset($changeArray[$k]);
482
			$toRemove[] = $k;
483
		}
484
485
	// Proceed with the deletion.
486
	if (!empty($toRemove))
487
		$smcFunc['db_query']('', '
488
			DELETE FROM {db_prefix}settings
489
			WHERE variable IN ({array_string:remove})',
490
			array(
491
				'remove' => $toRemove,
492
			)
493
		);
494
495
	// In some cases, this may be better and faster, but for large sets we don't want so many UPDATEs.
496
	if ($update)
497
	{
498
		foreach ($changeArray as $variable => $value)
499
		{
500
			$smcFunc['db_query']('', '
501
				UPDATE {db_prefix}settings
502
				SET value = {' . ($value === false || $value === true ? 'raw' : 'string') . ':value}
503
				WHERE variable = {string:variable}',
504
				array(
505
					'value' => $value === true ? 'value + 1' : ($value === false ? 'value - 1' : $value),
506
					'variable' => $variable,
507
				)
508
			);
509
			$modSettings[$variable] = $value === true ? $modSettings[$variable] + 1 : ($value === false ? $modSettings[$variable] - 1 : $value);
510
		}
511
512
		// Clean out the cache and make sure the cobwebs are gone too.
513
		cache_put_data('modSettings', null, 90);
514
515
		return;
516
	}
517
518
	$replaceArray = array();
519
	foreach ($changeArray as $variable => $value)
520
	{
521
		// Don't bother if it's already like that ;).
522
		if (isset($modSettings[$variable]) && $modSettings[$variable] == $value)
523
			continue;
524
		// If the variable isn't set, but would only be set to nothing'ness, then don't bother setting it.
525
		elseif (!isset($modSettings[$variable]) && empty($value))
526
			continue;
527
528
		$replaceArray[] = array($variable, $value);
529
530
		$modSettings[$variable] = $value;
531
	}
532
533
	if (empty($replaceArray))
534
		return;
535
536
	$smcFunc['db_insert']('replace',
537
		'{db_prefix}settings',
538
		array('variable' => 'string-255', 'value' => 'string-65534'),
539
		$replaceArray,
540
		array('variable')
541
	);
542
543
	// Kill the cache - it needs redoing now, but we won't bother ourselves with that here.
544
	cache_put_data('modSettings', null, 90);
545
}
546
547
/**
548
 * Constructs a page list.
549
 *
550
 * - builds the page list, e.g. 1 ... 6 7 [8] 9 10 ... 15.
551
 * - flexible_start causes it to use "url.page" instead of "url;start=page".
552
 * - very importantly, cleans up the start value passed, and forces it to
553
 *   be a multiple of num_per_page.
554
 * - checks that start is not more than max_value.
555
 * - base_url should be the URL without any start parameter on it.
556
 * - uses the compactTopicPagesEnable and compactTopicPagesContiguous
557
 *   settings to decide how to display the menu.
558
 *
559
 * an example is available near the function definition.
560
 * $pageindex = constructPageIndex($scripturl . '?board=' . $board, $_REQUEST['start'], $num_messages, $maxindex, true);
561
 *
562
 * @param string $base_url The basic URL to be used for each link.
563
 * @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.
564
 * @param int $max_value The total number of items you are paginating for.
565
 * @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.
566
 * @param bool $flexible_start Whether a ;start=x component should be introduced into the URL automatically (see above)
567
 * @param bool $show_prevnext Whether the Previous and Next links should be shown (should be on only when navigating the list)
568
 *
569
 * @return string The complete HTML of the page index that was requested, formatted by the template.
570
 */
571
function constructPageIndex($base_url, &$start, $max_value, $num_per_page, $flexible_start = false, $show_prevnext = true)
572
{
573
	global $modSettings, $context, $smcFunc, $settings, $txt;
574
575
	// Save whether $start was less than 0 or not.
576
	$start = (int) $start;
577
	$start_invalid = $start < 0;
578
579
	// Make sure $start is a proper variable - not less than 0.
580
	if ($start_invalid)
581
		$start = 0;
582
	// Not greater than the upper bound.
583
	elseif ($start >= $max_value)
584
		$start = max(0, (int) $max_value - (((int) $max_value % (int) $num_per_page) == 0 ? $num_per_page : ((int) $max_value % (int) $num_per_page)));
585
	// And it has to be a multiple of $num_per_page!
586
	else
587
		$start = max(0, (int) $start - ((int) $start % (int) $num_per_page));
588
589
	$context['current_page'] = $start / $num_per_page;
590
591
	// Define some default page index settings if we don't already have it...
592
	if (!isset($settings['page_index']))
593
	{
594
		// This defines the formatting for the page indexes used throughout the forum.
595
		$settings['page_index'] = array(
596
			'extra_before' => '<span class="pages">' . $txt['pages'] . '</span>',
597
			'previous_page' => '<span class="generic_icons previous_page"></span>',
598
			'current_page' => '<span class="current_page">%1$d</span> ',
599
			'page' => '<a class="navPages" href="{URL}">%2$s</a> ',
600
			'expand_pages' => '<span class="expand_pages" onclick="expandPages(this, {LINK}, {FIRST_PAGE}, {LAST_PAGE}, {PER_PAGE});"> ... </span>',
601
			'next_page' => '<span class="generic_icons next_page"></span>',
602
			'extra_after' => '',
603
		);
604
	}
605
606
	$base_link = strtr($settings['page_index']['page'], array('{URL}' => $flexible_start ? $base_url : strtr($base_url, array('%' => '%%')) . ';start=%1$d'));
607
	$pageindex = $settings['page_index']['extra_before'];
608
609
	// Compact pages is off or on?
610
	if (empty($modSettings['compactTopicPagesEnable']))
611
	{
612
		// Show the left arrow.
613
		$pageindex .= $start == 0 ? ' ' : sprintf($base_link, $start - $num_per_page, $settings['page_index']['previous_page']);
614
615
		// Show all the pages.
616
		$display_page = 1;
617
		for ($counter = 0; $counter < $max_value; $counter += $num_per_page)
618
			$pageindex .= $start == $counter && !$start_invalid ? sprintf($settings['page_index']['current_page'], $display_page++) : sprintf($base_link, $counter, $display_page++);
619
620
		// Show the right arrow.
621
		$display_page = ($start + $num_per_page) > $max_value ? $max_value : ($start + $num_per_page);
622
		if ($start != $counter - $max_value && !$start_invalid)
623
			$pageindex .= $display_page > $counter - $num_per_page ? ' ' : sprintf($base_link, $display_page, $settings['page_index']['next_page']);
624
	}
625
	else
626
	{
627
		// If they didn't enter an odd value, pretend they did.
628
		$PageContiguous = (int) ($modSettings['compactTopicPagesContiguous'] - ($modSettings['compactTopicPagesContiguous'] % 2)) / 2;
629
630
		// Show the "prev page" link. (>prev page< 1 ... 6 7 [8] 9 10 ... 15 next page)
631
		if (!empty($start) && $show_prevnext)
632
			$pageindex .= sprintf($base_link, $start - $num_per_page, $settings['page_index']['previous_page']);
633
		else
634
			$pageindex .= '';
635
636
		// Show the first page. (prev page >1< ... 6 7 [8] 9 10 ... 15)
637
		if ($start > $num_per_page * $PageContiguous)
638
			$pageindex .= sprintf($base_link, 0, '1');
639
640
		// Show the ... after the first page.  (prev page 1 >...< 6 7 [8] 9 10 ... 15 next page)
641
		if ($start > $num_per_page * ($PageContiguous + 1))
642
			$pageindex .= strtr($settings['page_index']['expand_pages'], array(
643
				'{LINK}' => JavaScriptEscape($smcFunc['htmlspecialchars']($base_link)),
644
				'{FIRST_PAGE}' => $num_per_page,
645
				'{LAST_PAGE}' => $start - $num_per_page * $PageContiguous,
646
				'{PER_PAGE}' => $num_per_page,
647
			));
648
649
		// Show the pages before the current one. (prev page 1 ... >6 7< [8] 9 10 ... 15 next page)
650
		for ($nCont = $PageContiguous; $nCont >= 1; $nCont--)
651
			if ($start >= $num_per_page * $nCont)
652
			{
653
				$tmpStart = $start - $num_per_page * $nCont;
654
				$pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
655
			}
656
657
		// Show the current page. (prev page 1 ... 6 7 >[8]< 9 10 ... 15 next page)
658
		if (!$start_invalid)
659
			$pageindex .= sprintf($settings['page_index']['current_page'], $start / $num_per_page + 1);
660
		else
661
			$pageindex .= sprintf($base_link, $start, $start / $num_per_page + 1);
662
663
		// Show the pages after the current one... (prev page 1 ... 6 7 [8] >9 10< ... 15 next page)
664
		$tmpMaxPages = (int) (($max_value - 1) / $num_per_page) * $num_per_page;
665
		for ($nCont = 1; $nCont <= $PageContiguous; $nCont++)
666
			if ($start + $num_per_page * $nCont <= $tmpMaxPages)
667
			{
668
				$tmpStart = $start + $num_per_page * $nCont;
669
				$pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
670
			}
671
672
		// Show the '...' part near the end. (prev page 1 ... 6 7 [8] 9 10 >...< 15 next page)
673
		if ($start + $num_per_page * ($PageContiguous + 1) < $tmpMaxPages)
674
			$pageindex .= strtr($settings['page_index']['expand_pages'], array(
675
				'{LINK}' => JavaScriptEscape($smcFunc['htmlspecialchars']($base_link)),
676
				'{FIRST_PAGE}' => $start + $num_per_page * ($PageContiguous + 1),
677
				'{LAST_PAGE}' => $tmpMaxPages,
678
				'{PER_PAGE}' => $num_per_page,
679
			));
680
681
		// Show the last number in the list. (prev page 1 ... 6 7 [8] 9 10 ... >15<  next page)
682
		if ($start + $num_per_page * $PageContiguous < $tmpMaxPages)
683
			$pageindex .= sprintf($base_link, $tmpMaxPages, $tmpMaxPages / $num_per_page + 1);
684
685
		// Show the "next page" link. (prev page 1 ... 6 7 [8] 9 10 ... 15 >next page<)
686
		if ($start != $tmpMaxPages && $show_prevnext)
687
			$pageindex .= sprintf($base_link, $start + $num_per_page, $settings['page_index']['next_page']);
688
	}
689
	$pageindex .= $settings['page_index']['extra_after'];
690
691
	return $pageindex;
692
}
693
694
/**
695
 * - Formats a number.
696
 * - uses the format of number_format to decide how to format the number.
697
 *   for example, it might display "1 234,50".
698
 * - caches the formatting data from the setting for optimization.
699
 *
700
 * @param float $number A number
701
 * @param bool|int $override_decimal_count If set, will use the specified number of decimal places. Otherwise it's automatically determined
702
 * @return string A formatted number
703
 */
704
function comma_format($number, $override_decimal_count = false)
705
{
706
	global $txt;
707
	static $thousands_separator = null, $decimal_separator = null, $decimal_count = null;
708
709
	// Cache these values...
710
	if ($decimal_separator === null)
711
	{
712
		// Not set for whatever reason?
713
		if (empty($txt['number_format']) || preg_match('~^1([^\d]*)?234([^\d]*)(0*?)$~', $txt['number_format'], $matches) != 1)
714
			return $number;
715
716
		// Cache these each load...
717
		$thousands_separator = $matches[1];
718
		$decimal_separator = $matches[2];
719
		$decimal_count = strlen($matches[3]);
720
	}
721
722
	// Format the string with our friend, number_format.
723
	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

723
	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...
724
}
725
726
/**
727
 * Format a time to make it look purdy.
728
 *
729
 * - returns a pretty formatted version of time based on the user's format in $user_info['time_format'].
730
 * - applies all necessary time offsets to the timestamp, unless offset_type is set.
731
 * - if todayMod is set and show_today was not not specified or true, an
732
 *   alternate format string is used to show the date with something to show it is "today" or "yesterday".
733
 * - performs localization (more than just strftime would do alone.)
734
 *
735
 * @param int $log_time A timestamp
736
 * @param bool $show_today Whether to show "Today"/"Yesterday" or just a date
737
 * @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.
738
 * @param bool $process_safe Activate setlocale check for changes at runtime. Slower, but safer.
739
 * @return string A formatted timestamp
740
 */
741
function timeformat($log_time, $show_today = true, $offset_type = false, $process_safe = false)
742
{
743
	global $context, $user_info, $txt, $modSettings;
744
	static $non_twelve_hour, $locale_cache;
745
	static $unsupportedFormats, $finalizedFormats;
746
747
	$unsupportedFormatsWindows = array('z','Z');
748
749
	// Ensure required values are set
750
	$user_info['time_offset'] = !empty($user_info['time_offset']) ? $user_info['time_offset'] : 0;
751
	$modSettings['time_offset'] = !empty($modSettings['time_offset']) ? $modSettings['time_offset'] : 0;
752
	$user_info['time_format'] = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %H:%M');
753
754
	// Offset the time.
755
	if (!$offset_type)
756
		$time = $log_time + ($user_info['time_offset'] + $modSettings['time_offset']) * 3600;
757
	// Just the forum offset?
758
	elseif ($offset_type == 'forum')
759
		$time = $log_time + $modSettings['time_offset'] * 3600;
760
	else
761
		$time = $log_time;
762
763
	// We can't have a negative date (on Windows, at least.)
764
	if ($log_time < 0)
765
		$log_time = 0;
766
767
	// Today and Yesterday?
768
	if ($modSettings['todayMod'] >= 1 && $show_today === true)
769
	{
770
		// Get the current time.
771
		$nowtime = forum_time();
772
773
		$then = @getdate($time);
774
		$now = @getdate($nowtime);
775
776
		// Try to make something of a time format string...
777
		$s = strpos($user_info['time_format'], '%S') === false ? '' : ':%S';
778
		if (strpos($user_info['time_format'], '%H') === false && strpos($user_info['time_format'], '%T') === false)
779
		{
780
			$h = strpos($user_info['time_format'], '%l') === false ? '%I' : '%l';
781
			$today_fmt = $h . ':%M' . $s . ' %p';
782
		}
783
		else
784
			$today_fmt = '%H:%M' . $s;
785
786
		// Same day of the year, same year.... Today!
787
		if ($then['yday'] == $now['yday'] && $then['year'] == $now['year'])
788
			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

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

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

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

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

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

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

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

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

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

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

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

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

5334
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5335
5336
	// remove left to right / right to left overrides
5337
	if ($num === 0x202D || $num === 0x202E)
5338
		return '';
5339
5340
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
5341
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
5342
		return '&#' . $num . ';';
5343
5344
	if (empty($context['utf8']))
5345
	{
5346
		// no control characters
5347
		if ($num < 0x20)
5348
			return '';
5349
		// text is text
5350
		elseif ($num < 0x80)
5351
			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

5351
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5352
		// all others get html-ised
5353
		else
5354
			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

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

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

5400
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5401
	// <0x800 (2048)
5402
	elseif ($num < 0x800)
5403
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5404
	// < 0x10000 (65536)
5405
	elseif ($num < 0x10000)
5406
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5407
	// <= 0x10FFFF (1114111)
5408
	else
5409
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5410
}
5411
5412
/**
5413
 * Strips out invalid html entities, replaces others with html style &#123; codes
5414
 *
5415
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
5416
 * strpos, strlen, substr etc
5417
 *
5418
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
5419
 * @return string The fixed string
5420
 */
5421
function entity_fix__callback($matches)
5422
{
5423
	if (!isset($matches[2]))
5424
		return '';
5425
5426
	$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

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

5506
	$later = (int) date_format(date_add(/** @scrutinizer ignore-type */ $date_when, date_interval_create_from_date_string('1 year')), 'U');
Loading history...
5507
5508
	// Load up any custom time zone descriptions we might have
5509
	loadLanguage('Timezones');
5510
5511
	// Should we put time zones from certain countries at the top of the list?
5512
	$priority_countries = !empty($modSettings['timezone_priority_countries']) ? explode(',', $modSettings['timezone_priority_countries']) : array();
5513
	$priority_tzids = array();
5514
	foreach ($priority_countries as $country)
5515
	{
5516
		$country_tzids = @timezone_identifiers_list(DateTimeZone::PER_COUNTRY, strtoupper(trim($country)));
5517
		if (!empty($country_tzids))
5518
			$priority_tzids = array_merge($priority_tzids, $country_tzids);
5519
	}
5520
5521
	// Antarctic research stations should be listed last, unless you're running a penguin forum
5522
	$low_priority_tzids = !in_array('AQ', $priority_countries) ? timezone_identifiers_list(DateTimeZone::ANTARCTICA) : array();
5523
5524
	// Process the preferred timezones first, then the normal ones, then the low priority ones.
5525
	$tzids = array_merge(array_keys($tztxt), array_diff(timezone_identifiers_list(), array_keys($tztxt), $low_priority_tzids), $low_priority_tzids);
0 ignored issues
show
Bug introduced by
It seems like $low_priority_tzids can also be of type false; however, parameter $_ of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

5525
	$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...
5526
5527
5528
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
5529
	foreach ($tzids as $tzid)
5530
	{
5531
		// We don't want UTC right now
5532
		if ($tzid == 'UTC')
5533
			continue;
5534
5535
		$tz = timezone_open($tzid);
5536
5537
		// First, get the set of transition rules for this tzid
5538
		$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

5538
		$tzinfo = timezone_transitions_get(/** @scrutinizer ignore-type */ $tz, $when, $later);
Loading history...
5539
5540
		// Use the entire set of transition rules as the array *key* so we can avoid duplicates
5541
		$tzkey = serialize($tzinfo);
5542
5543
		// Next, get the geographic info for this tzid
5544
		$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

5544
		$tzgeo = timezone_location_get(/** @scrutinizer ignore-type */ $tz);
Loading history...
5545
5546
		// Don't overwrite our preferred tzids
5547
		if (empty($zones[$tzkey]['tzid']))
5548
		{
5549
			$zones[$tzkey]['tzid'] = $tzid;
5550
			$zones[$tzkey]['abbr'] = $tzinfo[0]['abbr'];
5551
		}
5552
5553
		// A time zone from a prioritized country?
5554
		if (in_array($tzid, $priority_tzids))
5555
			$priority_zones[$tzkey] = true;
5556
5557
		// Keep track of the location and offset for this tzid
5558
		if (!empty($txt[$tzid]))
5559
			$zones[$tzkey]['locations'][] = $txt[$tzid];
5560
		else
5561
		{
5562
			$tzid_parts = explode('/', $tzid);
5563
			$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
5564
		}
5565
		$offsets[$tzkey] = $tzinfo[0]['offset'];
5566
		$longitudes[$tzkey] = empty($longitudes[$tzkey]) ? $tzgeo['longitude'] : $longitudes[$tzkey];
5567
	}
5568
5569
	// Sort by offset then longitude
5570
	array_multisort($offsets, SORT_ASC, SORT_NUMERIC, $longitudes, SORT_ASC, SORT_NUMERIC, $zones);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $longitudes does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $zones does not seem to be defined for all execution paths leading up to this point.
Loading history...
5571
5572
	// Build the final array of formatted values
5573
	$priority_timezones = array();
5574
	$timezones = array();
5575
	foreach ($zones as $tzkey => $tzvalue)
5576
	{
5577
		date_timezone_set($date_when, timezone_open($tzvalue['tzid']));
0 ignored issues
show
Bug introduced by
It seems like $date_when can also be of type false; however, parameter $object of date_timezone_set() does only seem to accept DateTime, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

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