Passed
Pull Request — release-2.1 (#6906)
by
unknown
06:54
created

template_dns_prefetch()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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

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

711
	return number_format($number, /** @scrutinizer ignore-type */ (float) $number === $number ? ($override_decimal_count === false ? $decimal_count : $override_decimal_count) : 0, $decimal_separator, $thousands_separator);
Loading history...
712
}
713
714
/**
715
 * Format a time to make it look purdy.
716
 *
717
 * - returns a pretty formatted version of time based on the user's format in $user_info['time_format'].
718
 * - applies all necessary time offsets to the timestamp, unless offset_type is set.
719
 * - if todayMod is set and show_today was not not specified or true, an
720
 *   alternate format string is used to show the date with something to show it is "today" or "yesterday".
721
 * - performs localization (more than just strftime would do alone.)
722
 *
723
 * @param int $log_time A timestamp
724
 * @param bool|string $show_today Whether to show "Today"/"Yesterday" or just a date. If a string is specified, that is used to temporarily override the date format.
725
 * @param bool|string $offset_type If false, uses both user time offset and forum offset. If 'forum', uses only the forum offset. Otherwise no offset is applied.
726
 * @param bool $process_safe Activate setlocale check for changes at runtime. Slower, but safer.
727
 * @return string A formatted timestamp
728
 */
729
function timeformat($log_time, $show_today = true, $offset_type = false, $process_safe = false)
730
{
731
	global $context, $user_info, $txt, $modSettings;
732
	static $non_twelve_hour, $locale, $now;
733
	static $unsupportedFormats, $finalizedFormats;
734
735
	$unsupportedFormatsWindows = array('z', 'Z');
736
737
	// Ensure required values are set
738
	$user_info['time_format'] = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %H:%M');
739
740
	// Offset the time.
741
	if (!$offset_type)
742
		$log_time = forum_time(true, $log_time);
743
	// Just the forum offset?
744
	elseif ($offset_type == 'forum')
745
		$log_time = forum_time(false, $log_time);
746
747
	// We can't have a negative date (on Windows, at least.)
748
	if ($log_time < 0)
749
		$log_time = 0;
750
751
	// Today and Yesterday?
752
	$prefix = '';
753
	if ($modSettings['todayMod'] >= 1 && $show_today === true)
754
	{
755
		$now_time = forum_time();
756
757
		if ($now_time - $log_time < (86400 * $modSettings['todayMod']))
758
		{
759
			$then = @getdate($log_time);
760
			$now = (!empty($now) ? $now : @getdate($now_time));
761
762
			// Same day of the year, same year.... Today!
763
			if ($then['yday'] == $now['yday'] && $then['year'] == $now['year'])
764
			{
765
				$prefix = $txt['today'];
766
			}
767
			// Day-of-year is one less and same year, or it's the first of the year and that's the last of the year...
768
			elseif ($modSettings['todayMod'] == '2' && (($then['yday'] == $now['yday'] - 1 && $then['year'] == $now['year']) || ($now['yday'] == 0 && $then['year'] == $now['year'] - 1) && $then['mon'] == 12 && $then['mday'] == 31))
769
			{
770
				$prefix = $txt['yesterday'];
771
			}
772
		}
773
	}
774
775
	// If $show_today is not a bool, use it as the date format & don't use $user_info. Allows for temp override of the format.
776
	$str = !is_bool($show_today) ? $show_today : $user_info['time_format'];
777
778
	// Use the cached formats if available
779
	if (is_null($finalizedFormats))
780
		$finalizedFormats = (array) cache_get_data('timeformatstrings', 86400);
781
782
	if (!isset($finalizedFormats[$str]) || !is_array($finalizedFormats[$str]))
783
		$finalizedFormats[$str] = array();
784
785
	// Make a supported version for this format if we don't already have one
786
	$format_type = !empty($prefix) ? 'time_only' : 'normal';
787
	if (empty($finalizedFormats[$str][$format_type]))
788
	{
789
		$timeformat = $format_type == 'time_only' ? get_date_or_time_format('time', $str) : $str;
790
791
		// Not all systems support all formats, and Windows fails altogether if unsupported ones are
792
		// used, so let's prevent that. Some substitutions go to the nearest reasonable fallback, some
793
		// turn into static strings, some (i.e. %a, %A, %b, %B, %p) have special handling below.
794
		$strftimeFormatSubstitutions = array(
795
			// Day
796
			'a' => '#txt_days_short_%w#', 'A' => '#txt_days_%w#', 'e' => '%d', 'd' => '&#37;d', 'j' => '&#37;j', 'u' => '%w', 'w' => '&#37;w',
797
			// Week
798
			'U' => '&#37;U', 'V' => '%U', 'W' => '%U',
799
			// Month
800
			'b' => '#txt_months_short_%m#', 'B' => '#txt_months_%m#', 'h' => '%b', 'm' => '&#37;m',
801
			// Year
802
			'C' => '&#37;C', 'g' => '%y', 'G' => '%Y', 'y' => '&#37;y', 'Y' => '&#37;Y',
803
			// Time
804
			'H' => '&#37;H', 'k' => '%H', 'I' => '%H', 'l' => '%I', 'M' => '&#37;M', 'p' => '&#37;p', 'P' => '%p',
805
			'r' => '%I:%M:%S %p', 'R' => '%H:%M', 'S' => '&#37;S', 'T' => '%H:%M:%S', 'X' => '%T', 'z' => '&#37;z', 'Z' => '&#37;Z',
806
			// Time and Date Stamps
807
			'c' => '%F %T', 'D' => '%m/%d/%y', 'F' => '%Y-%m-%d', 's' => '&#37;s', 'x' => '%F',
808
			// Miscellaneous
809
			'n' => "\n", 't' => "\t", '%' => '&#37;',
810
		);
811
812
		// No need to do this part again if we already did it once
813
		if (is_null($unsupportedFormats))
814
			$unsupportedFormats = (array) cache_get_data('unsupportedtimeformats', 86400);
815
		if (empty($unsupportedFormats))
816
		{
817
			foreach ($strftimeFormatSubstitutions as $format => $substitution)
818
			{
819
				// Avoid a crashing bug with PHP 7 on certain versions of Windows
820
				if ($context['server']['is_windows'] && in_array($format, $unsupportedFormatsWindows))
821
				{
822
					$unsupportedFormats[] = $format;
823
					continue;
824
				}
825
826
				$value = @strftime('%' . $format);
827
828
				// Windows will return false for unsupported formats
829
				// Other operating systems return the format string as a literal
830
				if ($value === false || $value === $format)
831
					$unsupportedFormats[] = $format;
832
			}
833
			cache_put_data('unsupportedtimeformats', $unsupportedFormats, 86400);
834
		}
835
836
		// Windows needs extra help if $timeformat contains something completely invalid, e.g. '%Q'
837
		if (DIRECTORY_SEPARATOR === '\\')
838
			$timeformat = preg_replace('~%(?!' . implode('|', array_keys($strftimeFormatSubstitutions)) . ')~', '&#37;', $timeformat);
839
840
		// Substitute unsupported formats with supported ones
841
		if (!empty($unsupportedFormats))
842
			while (preg_match('~%(' . implode('|', $unsupportedFormats) . ')~', $timeformat, $matches))
843
				$timeformat = str_replace($matches[0], $strftimeFormatSubstitutions[$matches[1]], $timeformat);
844
845
		// Remember this so we don't need to do it again
846
		$finalizedFormats[$str][$format_type] = $timeformat;
847
		cache_put_data('timeformatstrings', $finalizedFormats, 86400);
848
	}
849
850
	$timeformat = $finalizedFormats[$str][$format_type];
851
852
	// Windows requires a slightly different language code identifier (LCID).
853
	// https://msdn.microsoft.com/en-us/library/cc233982.aspx
854
	$lang_locale = $context['server']['is_windows'] ? strtr($txt['lang_locale'], '_', '-') : $txt['lang_locale'];
855
856
	// Make sure we are using the correct locale.
857
	if (!isset($locale) || ($process_safe === true && setlocale(LC_TIME, '0') != $locale))
858
		$locale = setlocale(LC_TIME, array($lang_locale . '.' . $modSettings['global_character_set'], $lang_locale . '.' . $txt['lang_character_set'], $lang_locale));
859
860
	// If the current locale is unsupported, we'll have to localize the hard way.
861
	if ($locale === false)
862
	{
863
		$timeformat = strtr($timeformat, array(
864
			'%a' => '#txt_days_short_%w#',
865
			'%A' => '#txt_days_%w#',
866
			'%b' => '#txt_months_short_%m#',
867
			'%B' => '#txt_months_%m#',
868
			'%p' => '&#37;p',
869
			'%P' => '&#37;p'
870
		));
871
	}
872
	// Just in case the locale doesn't support '%p' properly.
873
	// @todo Is this even necessary?
874
	else
875
	{
876
		if (!isset($non_twelve_hour) && strpos($timeformat, '%p') !== false)
877
			$non_twelve_hour = trim(strftime('%p')) === '';
878
879
		if (!empty($non_twelve_hour))
880
			$timeformat = strtr($timeformat, array(
881
				'%p' => '&#37;p',
882
				'%P' => '&#37;p'
883
			));
884
	}
885
886
	// And now, the moment we've all be waiting for...
887
	$timestring = strftime($timeformat, $log_time);
888
889
	// Do-it-yourself time localization.  Fun.
890
	if (strpos($timestring, '&#37;p') !== false)
891
		$timestring = str_replace('&#37;p', (strftime('%H', $log_time) < 12 ? $txt['time_am'] : $txt['time_pm']), $timestring);
892
	if (strpos($timestring, '#txt_') !== false)
893
	{
894
		if (strpos($timestring, '#txt_days_short_') !== false)
895
			$timestring = strtr($timestring, array(
896
				'#txt_days_short_0#' => $txt['days_short'][0],
897
				'#txt_days_short_1#' => $txt['days_short'][1],
898
				'#txt_days_short_2#' => $txt['days_short'][2],
899
				'#txt_days_short_3#' => $txt['days_short'][3],
900
				'#txt_days_short_4#' => $txt['days_short'][4],
901
				'#txt_days_short_5#' => $txt['days_short'][5],
902
				'#txt_days_short_6#' => $txt['days_short'][6],
903
			));
904
905
		if (strpos($timestring, '#txt_days_') !== false)
906
			$timestring = strtr($timestring, array(
907
				'#txt_days_0#' => $txt['days'][0],
908
				'#txt_days_1#' => $txt['days'][1],
909
				'#txt_days_2#' => $txt['days'][2],
910
				'#txt_days_3#' => $txt['days'][3],
911
				'#txt_days_4#' => $txt['days'][4],
912
				'#txt_days_5#' => $txt['days'][5],
913
				'#txt_days_6#' => $txt['days'][6],
914
			));
915
916
		if (strpos($timestring, '#txt_months_short_') !== false)
917
			$timestring = strtr($timestring, array(
918
				'#txt_months_short_01#' => $txt['months_short'][1],
919
				'#txt_months_short_02#' => $txt['months_short'][2],
920
				'#txt_months_short_03#' => $txt['months_short'][3],
921
				'#txt_months_short_04#' => $txt['months_short'][4],
922
				'#txt_months_short_05#' => $txt['months_short'][5],
923
				'#txt_months_short_06#' => $txt['months_short'][6],
924
				'#txt_months_short_07#' => $txt['months_short'][7],
925
				'#txt_months_short_08#' => $txt['months_short'][8],
926
				'#txt_months_short_09#' => $txt['months_short'][9],
927
				'#txt_months_short_10#' => $txt['months_short'][10],
928
				'#txt_months_short_11#' => $txt['months_short'][11],
929
				'#txt_months_short_12#' => $txt['months_short'][12],
930
			));
931
932
		if (strpos($timestring, '#txt_months_') !== false)
933
			$timestring = strtr($timestring, array(
934
				'#txt_months_01#' => $txt['months'][1],
935
				'#txt_months_02#' => $txt['months'][2],
936
				'#txt_months_03#' => $txt['months'][3],
937
				'#txt_months_04#' => $txt['months'][4],
938
				'#txt_months_05#' => $txt['months'][5],
939
				'#txt_months_06#' => $txt['months'][6],
940
				'#txt_months_07#' => $txt['months'][7],
941
				'#txt_months_08#' => $txt['months'][8],
942
				'#txt_months_09#' => $txt['months'][9],
943
				'#txt_months_10#' => $txt['months'][10],
944
				'#txt_months_11#' => $txt['months'][11],
945
				'#txt_months_12#' => $txt['months'][12],
946
			));
947
	}
948
949
	// Restore any literal percent characters, add the prefix, and we're done.
950
	return $prefix . str_replace('&#37;', '%', $timestring);
951
}
952
953
/**
954
 * Gets a version of a strftime() format that only shows the date or time components
955
 *
956
 * @param string $type Either 'date' or 'time'.
957
 * @param string $format A strftime() format to process. Defaults to $user_info['time_format'].
958
 * @return string A strftime() format string
959
 */
960
function get_date_or_time_format($type = '', $format = '')
961
{
962
	global $user_info, $modSettings;
963
	static $formats;
964
965
	// If the format is invalid, fall back to defaults.
966
	if (strpos($format, '%') === false)
967
		$format = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %k:%M');
968
969
	$orig_format = $format;
970
971
	// Have we already done this?
972
	if (isset($formats[$orig_format][$type]))
973
		return $formats[$orig_format][$type];
974
975
	if ($type === 'date')
976
	{
977
		$specifications = array(
978
			// Day
979
			'%a' => '%a', '%A' => '%A', '%e' => '%e', '%d' => '%d', '%j' => '%j', '%u' => '%u', '%w' => '%w',
980
			// Week
981
			'%U' => '%U', '%V' => '%V', '%W' => '%W',
982
			// Month
983
			'%b' => '%b', '%B' => '%B', '%h' => '%h', '%m' => '%m',
984
			// Year
985
			'%C' => '%C', '%g' => '%g', '%G' => '%G', '%y' => '%y', '%Y' => '%Y',
986
			// Time
987
			'%H' => '', '%k' => '', '%I' => '', '%l' => '', '%M' => '', '%p' => '', '%P' => '',
988
			'%r' => '', '%R' => '', '%S' => '', '%T' => '', '%X' => '', '%z' => '', '%Z' => '',
989
			// Time and Date Stamps
990
			'%c' => '%x', '%D' => '%D', '%F' => '%F', '%s' => '%s', '%x' => '%x',
991
			// Miscellaneous
992
			'%n' => '', '%t' => '', '%%' => '%%',
993
		);
994
995
		$default_format = '%F';
996
	}
997
	elseif ($type === 'time')
998
	{
999
		$specifications = array(
1000
			// Day
1001
			'%a' => '', '%A' => '', '%e' => '', '%d' => '', '%j' => '', '%u' => '', '%w' => '',
1002
			// Week
1003
			'%U' => '', '%V' => '', '%W' => '',
1004
			// Month
1005
			'%b' => '', '%B' => '', '%h' => '', '%m' => '',
1006
			// Year
1007
			'%C' => '', '%g' => '', '%G' => '', '%y' => '', '%Y' => '',
1008
			// Time
1009
			'%H' => '%H', '%k' => '%k', '%I' => '%I', '%l' => '%l', '%M' => '%M', '%p' => '%p', '%P' => '%P',
1010
			'%r' => '%r', '%R' => '%R', '%S' => '%S', '%T' => '%T', '%X' => '%X', '%z' => '%z', '%Z' => '%Z',
1011
			// Time and Date Stamps
1012
			'%c' => '%X', '%D' => '', '%F' => '', '%s' => '%s', '%x' => '',
1013
			// Miscellaneous
1014
			'%n' => '', '%t' => '', '%%' => '%%',
1015
		);
1016
1017
		$default_format = '%k:%M';
1018
	}
1019
	// Invalid type requests just get the full format string.
1020
	else
1021
		return $format;
1022
1023
	// Separate the specifications we want from the ones we don't.
1024
	$wanted = array_filter($specifications);
1025
	$unwanted = array_diff(array_keys($specifications), $wanted);
1026
1027
	// First, make any necessary substitutions in the format.
1028
	$format = strtr($format, $wanted);
1029
1030
	// Next, strip out any specifications and literal text that we don't want.
1031
	$format_parts = preg_split('~%[' . (strtr(implode('', $unwanted), array('%' => ''))) . ']~u', $format);
1032
1033
	foreach ($format_parts as $p => $f)
1034
	{
1035
		if (strpos($f, '%') === false)
1036
			unset($format_parts[$p]);
1037
	}
1038
1039
	$format = implode('', $format_parts);
1040
1041
	// Finally, strip out any unwanted leftovers.
1042
	// For info on the charcter classes used here, see https://www.php.net/manual/en/regexp.reference.unicode.php and https://www.regular-expressions.info/unicode.html
1043
	$format = preg_replace(
1044
		array(
1045
			// Anything that isn't a specification, punctuation mark, or whitespace.
1046
			'~(?<!%)\p{L}|[^\p{L}\p{P}\s]~u',
1047
			// A series of punctuation marks (except %), possibly separated by whitespace.
1048
			'~([^%\P{P}])(\s*)(?'.'>(\1|[^%\P{Po}])\s*(?!$))*~u',
1049
			// Unwanted trailing punctuation and whitespace.
1050
			'~(?'.'>([\p{Pd}\p{Ps}\p{Pi}\p{Pc}]|[^%\P{Po}])\s*)*$~u',
1051
			// Unwanted opening punctuation and whitespace.
1052
			'~^\s*(?'.'>([\p{Pd}\p{Pe}\p{Pf}\p{Pc}]|[^%\P{Po}])\s*)*~u',
1053
		),
1054
		array(
1055
			'',
1056
			'$1$2',
1057
			'',
1058
			'',
1059
		),
1060
		$format
1061
	);
1062
1063
	// Gotta have something...
1064
	if (empty($format))
1065
		$format = $default_format;
1066
1067
	// Remember what we've done.
1068
	$formats[$orig_format][$type] = trim($format);
1069
1070
	return $formats[$orig_format][$type];
1071
}
1072
1073
/**
1074
 * Replaces special entities in strings with the real characters.
1075
 *
1076
 * Functionally equivalent to htmlspecialchars_decode(), except that this also
1077
 * replaces '&nbsp;' with a simple space character.
1078
 *
1079
 * @param string $string A string
1080
 * @return string The string without entities
1081
 */
1082
function un_htmlspecialchars($string)
1083
{
1084
	global $context;
1085
	static $translation = array();
1086
1087
	// Determine the character set... Default to UTF-8
1088
	if (empty($context['character_set']))
1089
		$charset = 'UTF-8';
1090
	// Use ISO-8859-1 in place of non-supported ISO-8859 charsets...
1091
	elseif (strpos($context['character_set'], 'ISO-8859-') !== false && !in_array($context['character_set'], array('ISO-8859-5', 'ISO-8859-15')))
1092
		$charset = 'ISO-8859-1';
1093
	else
1094
		$charset = $context['character_set'];
1095
1096
	if (empty($translation))
1097
		$translation = array_flip(get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES, $charset)) + array('&#039;' => '\'', '&#39;' => '\'', '&nbsp;' => ' ');
1098
1099
	return strtr($string, $translation);
1100
}
1101
1102
/**
1103
 * Shorten a subject + internationalization concerns.
1104
 *
1105
 * - shortens a subject so that it is either shorter than length, or that length plus an ellipsis.
1106
 * - respects internationalization characters and entities as one character.
1107
 * - avoids trailing entities.
1108
 * - returns the shortened string.
1109
 *
1110
 * @param string $subject The subject
1111
 * @param int $len How many characters to limit it to
1112
 * @return string The shortened subject - either the entire subject (if it's <= $len) or the subject shortened to $len characters with "..." appended
1113
 */
1114
function shorten_subject($subject, $len)
1115
{
1116
	global $smcFunc;
1117
1118
	// It was already short enough!
1119
	if ($smcFunc['strlen']($subject) <= $len)
1120
		return $subject;
1121
1122
	// Shorten it by the length it was too long, and strip off junk from the end.
1123
	return $smcFunc['substr']($subject, 0, $len) . '...';
1124
}
1125
1126
/**
1127
 * Gets the current time with offset.
1128
 *
1129
 * - always applies the offset in the time_offset setting.
1130
 *
1131
 * @param bool $use_user_offset Whether to apply the user's offset as well
1132
 * @param int $timestamp A timestamp (null to use current time)
1133
 * @return int Seconds since the unix epoch, with forum time offset and (optionally) user time offset applied
1134
 */
1135
function forum_time($use_user_offset = true, $timestamp = null)
1136
{
1137
	global $user_info, $modSettings;
1138
1139
	// Ensure required values are set
1140
	$modSettings['time_offset'] = !empty($modSettings['time_offset']) ? $modSettings['time_offset'] : 0;
1141
	$user_info['time_offset'] = !empty($user_info['time_offset']) ? $user_info['time_offset'] : 0;
1142
1143
	if ($timestamp === null)
1144
		$timestamp = time();
1145
	elseif ($timestamp == 0)
1146
		return 0;
1147
1148
	return $timestamp + ($modSettings['time_offset'] + ($use_user_offset ? $user_info['time_offset'] : 0)) * 3600;
1149
}
1150
1151
/**
1152
 * Calculates all the possible permutations (orders) of array.
1153
 * should not be called on huge arrays (bigger than like 10 elements.)
1154
 * returns an array containing each permutation.
1155
 *
1156
 * @deprecated since 2.1
1157
 * @param array $array An array
1158
 * @return array An array containing each permutation
1159
 */
1160
function permute($array)
1161
{
1162
	$orders = array($array);
1163
1164
	$n = count($array);
1165
	$p = range(0, $n);
1166
	for ($i = 1; $i < $n; null)
1167
	{
1168
		$p[$i]--;
1169
		$j = $i % 2 != 0 ? $p[$i] : 0;
1170
1171
		$temp = $array[$i];
1172
		$array[$i] = $array[$j];
1173
		$array[$j] = $temp;
1174
1175
		for ($i = 1; $p[$i] == 0; $i++)
1176
			$p[$i] = 1;
1177
1178
		$orders[] = $array;
1179
	}
1180
1181
	return $orders;
1182
}
1183
1184
/**
1185
 * Parse bulletin board code in a string, as well as smileys optionally.
1186
 *
1187
 * - only parses bbc tags which are not disabled in disabledBBC.
1188
 * - handles basic HTML, if enablePostHTML is on.
1189
 * - caches the from/to replace regular expressions so as not to reload them every time a string is parsed.
1190
 * - only parses smileys if smileys is true.
1191
 * - does nothing if the enableBBC setting is off.
1192
 * - uses the cache_id as a unique identifier to facilitate any caching it may do.
1193
 * - returns the modified message.
1194
 *
1195
 * @param string|bool $message The message.
1196
 *		When a empty string, nothing is done.
1197
 *		When false we provide a list of BBC codes available.
1198
 *		When a string, the message is parsed and bbc handled.
1199
 * @param bool $smileys Whether to parse smileys as well
1200
 * @param string $cache_id The cache ID
1201
 * @param array $parse_tags If set, only parses these tags rather than all of them
1202
 * @return string The parsed message
1203
 */
1204
function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = array())
1205
{
1206
	global $smcFunc, $txt, $scripturl, $context, $modSettings, $user_info, $sourcedir, $cache_enable;
1207
	static $bbc_lang_locales = array(), $itemcodes = array(), $no_autolink_tags = array();
1208
	static $disabled, $alltags_regex = '', $param_regexes = array(), $url_regex = '';
1209
1210
	// Don't waste cycles
1211
	if ($message === '')
1212
		return '';
1213
1214
	// Just in case it wasn't determined yet whether UTF-8 is enabled.
1215
	if (!isset($context['utf8']))
1216
		$context['utf8'] = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8';
1217
1218
	// Clean up any cut/paste issues we may have
1219
	$message = sanitizeMSCutPaste($message);
1220
1221
	// If the load average is too high, don't parse the BBC.
1222
	if (!empty($context['load_average']) && !empty($modSettings['bbc']) && $context['load_average'] >= $modSettings['bbc'])
1223
	{
1224
		$context['disabled_parse_bbc'] = true;
1225
		return $message;
1226
	}
1227
1228
	if ($smileys !== null && ($smileys == '1' || $smileys == '0'))
1229
		$smileys = (bool) $smileys;
1230
1231
	if (empty($modSettings['enableBBC']) && $message !== false)
1232
	{
1233
		if ($smileys === true)
1234
			parsesmileys($message);
1235
1236
		return $message;
1237
	}
1238
1239
	// If we already have a version of the BBCodes for the current language, use that. Otherwise, make one.
1240
	if (!empty($bbc_lang_locales[$txt['lang_locale']]))
1241
		$bbc_codes = $bbc_lang_locales[$txt['lang_locale']];
1242
	else
1243
		$bbc_codes = array();
1244
1245
	// If we are not doing every tag then we don't cache this run.
1246
	if (!empty($parse_tags))
1247
		$bbc_codes = array();
1248
1249
	// Ensure $modSettings['tld_regex'] contains a valid regex for the autolinker
1250
	if (!empty($modSettings['autoLinkUrls']))
1251
		set_tld_regex();
1252
1253
	// Allow mods access before entering the main parse_bbc loop
1254
	call_integration_hook('integrate_pre_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
1255
1256
	// Sift out the bbc for a performance improvement.
1257
	if (empty($bbc_codes) || $message === false || !empty($parse_tags))
1258
	{
1259
		if (!empty($modSettings['disabledBBC']))
1260
		{
1261
			$disabled = array();
1262
1263
			$temp = explode(',', strtolower($modSettings['disabledBBC']));
1264
1265
			foreach ($temp as $tag)
1266
				$disabled[trim($tag)] = true;
1267
1268
			if (in_array('color', $disabled))
1269
				$disabled = array_merge($disabled, array(
1270
					'black' => true,
1271
					'white' => true,
1272
					'red' => true,
1273
					'green' => true,
1274
					'blue' => true,
1275
					)
1276
				);
1277
		}
1278
1279
		// The YouTube bbc needs this for its origin parameter
1280
		$scripturl_parts = parse_url($scripturl);
1281
		$hosturl = $scripturl_parts['scheme'] . '://' . $scripturl_parts['host'];
1282
1283
		/* The following bbc are formatted as an array, with keys as follows:
1284
1285
			tag: the tag's name - should be lowercase!
1286
1287
			type: one of...
1288
				- (missing): [tag]parsed content[/tag]
1289
				- unparsed_equals: [tag=xyz]parsed content[/tag]
1290
				- parsed_equals: [tag=parsed data]parsed content[/tag]
1291
				- unparsed_content: [tag]unparsed content[/tag]
1292
				- closed: [tag], [tag/], [tag /]
1293
				- unparsed_commas: [tag=1,2,3]parsed content[/tag]
1294
				- unparsed_commas_content: [tag=1,2,3]unparsed content[/tag]
1295
				- unparsed_equals_content: [tag=...]unparsed content[/tag]
1296
1297
			parameters: an optional array of parameters, for the form
1298
			  [tag abc=123]content[/tag].  The array is an associative array
1299
			  where the keys are the parameter names, and the values are an
1300
			  array which may contain the following:
1301
				- match: a regular expression to validate and match the value.
1302
				- quoted: true if the value should be quoted.
1303
				- validate: callback to evaluate on the data, which is $data.
1304
				- value: a string in which to replace $1 with the data.
1305
					Either value or validate may be used, not both.
1306
				- optional: true if the parameter is optional.
1307
				- default: a default value for missing optional parameters.
1308
1309
			test: a regular expression to test immediately after the tag's
1310
			  '=', ' ' or ']'.  Typically, should have a \] at the end.
1311
			  Optional.
1312
1313
			content: only available for unparsed_content, closed,
1314
			  unparsed_commas_content, and unparsed_equals_content.
1315
			  $1 is replaced with the content of the tag.  Parameters
1316
			  are replaced in the form {param}.  For unparsed_commas_content,
1317
			  $2, $3, ..., $n are replaced.
1318
1319
			before: only when content is not used, to go before any
1320
			  content.  For unparsed_equals, $1 is replaced with the value.
1321
			  For unparsed_commas, $1, $2, ..., $n are replaced.
1322
1323
			after: similar to before in every way, except that it is used
1324
			  when the tag is closed.
1325
1326
			disabled_content: used in place of content when the tag is
1327
			  disabled.  For closed, default is '', otherwise it is '$1' if
1328
			  block_level is false, '<div>$1</div>' elsewise.
1329
1330
			disabled_before: used in place of before when disabled.  Defaults
1331
			  to '<div>' if block_level, '' if not.
1332
1333
			disabled_after: used in place of after when disabled.  Defaults
1334
			  to '</div>' if block_level, '' if not.
1335
1336
			block_level: set to true the tag is a "block level" tag, similar
1337
			  to HTML.  Block level tags cannot be nested inside tags that are
1338
			  not block level, and will not be implicitly closed as easily.
1339
			  One break following a block level tag may also be removed.
1340
1341
			trim: if set, and 'inside' whitespace after the begin tag will be
1342
			  removed.  If set to 'outside', whitespace after the end tag will
1343
			  meet the same fate.
1344
1345
			validate: except when type is missing or 'closed', a callback to
1346
			  validate the data as $data.  Depending on the tag's type, $data
1347
			  may be a string or an array of strings (corresponding to the
1348
			  replacement.)
1349
1350
			quoted: when type is 'unparsed_equals' or 'parsed_equals' only,
1351
			  may be not set, 'optional', or 'required' corresponding to if
1352
			  the content may be quoted.  This allows the parser to read
1353
			  [tag="abc]def[esdf]"] properly.
1354
1355
			require_parents: an array of tag names, or not set.  If set, the
1356
			  enclosing tag *must* be one of the listed tags, or parsing won't
1357
			  occur.
1358
1359
			require_children: similar to require_parents, if set children
1360
			  won't be parsed if they are not in the list.
1361
1362
			disallow_children: similar to, but very different from,
1363
			  require_children, if it is set the listed tags will not be
1364
			  parsed inside the tag.
1365
1366
			parsed_tags_allowed: an array restricting what BBC can be in the
1367
			  parsed_equals parameter, if desired.
1368
		*/
1369
1370
		$codes = array(
1371
			array(
1372
				'tag' => 'abbr',
1373
				'type' => 'unparsed_equals',
1374
				'before' => '<abbr title="$1">',
1375
				'after' => '</abbr>',
1376
				'quoted' => 'optional',
1377
				'disabled_after' => ' ($1)',
1378
			),
1379
			// Legacy (and just an alias for [abbr] even when enabled)
1380
			array(
1381
				'tag' => 'acronym',
1382
				'type' => 'unparsed_equals',
1383
				'before' => '<abbr title="$1">',
1384
				'after' => '</abbr>',
1385
				'quoted' => 'optional',
1386
				'disabled_after' => ' ($1)',
1387
			),
1388
			array(
1389
				'tag' => 'anchor',
1390
				'type' => 'unparsed_equals',
1391
				'test' => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]',
1392
				'before' => '<span id="post_$1">',
1393
				'after' => '</span>',
1394
			),
1395
			array(
1396
				'tag' => 'attach',
1397
				'type' => 'unparsed_content',
1398
				'parameters' => array(
1399
					'id' => array('match' => '(\d+)'),
1400
					'alt' => array('optional' => true),
1401
					'width' => array('optional' => true, 'match' => '(\d+)'),
1402
					'height' => array('optional' => true, 'match' => '(\d+)'),
1403
					'display' => array('optional' => true, 'match' => '(link|embed)'),
1404
				),
1405
				'content' => '$1',
1406
				'validate' => function(&$tag, &$data, $disabled, $params) use ($modSettings, $context, $sourcedir, $txt, $smcFunc)
0 ignored issues
show
Unused Code introduced by
The import $context is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
1407
				{
1408
					$returnContext = '';
1409
1410
					// BBC or the entire attachments feature is disabled
1411
					if (empty($modSettings['attachmentEnable']) || !empty($disabled['attach']))
1412
						return $data;
1413
1414
					// Save the attach ID.
1415
					$attachID = $params['{id}'];
1416
1417
					// Kinda need this.
1418
					require_once($sourcedir . '/Subs-Attachments.php');
1419
1420
					$currentAttachment = parseAttachBBC($attachID);
1421
1422
					// parseAttachBBC will return a string ($txt key) rather than dying with a fatal_error. Up to you to decide what to do.
1423
					if (is_string($currentAttachment))
1424
						return $data = !empty($txt[$currentAttachment]) ? $txt[$currentAttachment] : $currentAttachment;
1425
1426
					// We need a display mode.
1427
					if (empty($params['{display}']))
1428
					{
1429
						// Images, video, and audio are embedded by default.
1430
						if (!empty($currentAttachment['is_image']) || strpos($currentAttachment['mime_type'], 'video/') === 0 || strpos($currentAttachment['mime_type'], 'audio/') === 0)
1431
							$params['{display}'] = 'embed';
1432
						// Anything else shows a link by default.
1433
						else
1434
							$params['{display}'] = 'link';
1435
					}
1436
1437
					// Embedded file.
1438
					if ($params['{display}'] == 'embed')
1439
					{
1440
						$alt = ' alt="' . (!empty($params['{alt}']) ? $params['{alt}'] : $currentAttachment['name']) . '"';
1441
						$title = !empty($data) ? ' title="' . $smcFunc['htmlspecialchars']($data) . '"' : '';
1442
1443
						// Image.
1444
						if (!empty($currentAttachment['is_image']))
1445
						{
1446
							if (empty($params['{width}']) && empty($params['{height}']))
1447
								$returnContext .= '<img src="' . $currentAttachment['href'] . '"' . $alt . $title . ' class="bbc_img">';
1448
							else
1449
							{
1450
								$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"': '';
1451
								$height = !empty($params['{height}']) ? 'height="' . $params['{height}'] . '"' : '';
1452
								$returnContext .= '<img src="' . $currentAttachment['href'] . ';image"' . $alt . $title . $width . $height . ' class="bbc_img resized"/>';
1453
							}
1454
						}
1455
						// Video.
1456
						elseif (strpos($currentAttachment['mime_type'], 'video/') === 0)
1457
						{
1458
							$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"' : '';
1459
							$height = !empty($params['{height}']) ? ' height="' . $params['{height}'] . '"' : '';
1460
1461
							$returnContext .= '<div class="videocontainer"><video controls preload="metadata" src="'. $currentAttachment['href'] . '" playsinline' . $width . $height . '><a href="' . $currentAttachment['href'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . '</a></video></div>' . (!empty($data) && $data != $currentAttachment['name'] ? '<div class="smalltext">' . $data . '</div>' : '');
1462
						}
1463
						// Audio.
1464
						elseif (strpos($currentAttachment['mime_type'], 'audio/') === 0)
1465
						{
1466
							$width = 'max-width:100%; width: ' . (!empty($params['{width}']) ? $params['{width}'] : '400') . 'px;';
1467
							$height = !empty($params['{height}']) ? 'height: ' . $params['{height}'] . 'px;' : '';
1468
1469
							$returnContext .= (!empty($data) && $data != $currentAttachment['name'] ? $data . ' ' : '') . '<audio controls preload="none" src="'. $currentAttachment['href'] . '" class="bbc_audio" style="vertical-align:middle;' . $width . $height . '"><a href="' . $currentAttachment['href'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . '</a></audio>';
1470
						}
1471
						// Anything else.
1472
						else
1473
						{
1474
							$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"' : '';
1475
							$height = !empty($params['{height}']) ? ' height="' . $params['{height}'] . '"' : '';
1476
1477
							$returnContext .= '<object type="' . $currentAttachment['mime_type'] . '" data="' . $currentAttachment['href'] . '"' . $width . $height . ' typemustmatch><a href="' . $currentAttachment['href'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . '</a></object>';
1478
						}
1479
					}
1480
1481
					// No image. Show a link.
1482
					else
1483
						$returnContext .= '<a href="' . $currentAttachment['href'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . '</a>';
1484
1485
					// Use this hook to adjust the HTML output of the attach BBCode.
1486
					// If you want to work with the attachment data itself, use one of these:
1487
					// - integrate_pre_parseAttachBBC
1488
					// - integrate_post_parseAttachBBC
1489
					call_integration_hook('integrate_attach_bbc_validate', array(&$returnContext, $currentAttachment, $tag, $data, $disabled, $params));
1490
1491
					// Gotta append what we just did.
1492
					$data = $returnContext;
1493
				},
1494
			),
1495
			array(
1496
				'tag' => 'b',
1497
				'before' => '<b>',
1498
				'after' => '</b>',
1499
			),
1500
			// Legacy (equivalent to [ltr] or [rtl])
1501
			array(
1502
				'tag' => 'bdo',
1503
				'type' => 'unparsed_equals',
1504
				'before' => '<bdo dir="$1">',
1505
				'after' => '</bdo>',
1506
				'test' => '(rtl|ltr)\]',
1507
				'block_level' => true,
1508
			),
1509
			// Legacy (alias of [color=black])
1510
			array(
1511
				'tag' => 'black',
1512
				'before' => '<span style="color: black;" class="bbc_color">',
1513
				'after' => '</span>',
1514
			),
1515
			// Legacy (alias of [color=blue])
1516
			array(
1517
				'tag' => 'blue',
1518
				'before' => '<span style="color: blue;" class="bbc_color">',
1519
				'after' => '</span>',
1520
			),
1521
			array(
1522
				'tag' => 'br',
1523
				'type' => 'closed',
1524
				'content' => '<br>',
1525
			),
1526
			array(
1527
				'tag' => 'center',
1528
				'before' => '<div class="centertext">',
1529
				'after' => '</div>',
1530
				'block_level' => true,
1531
			),
1532
			array(
1533
				'tag' => 'code',
1534
				'type' => 'unparsed_content',
1535
				'content' => '<div class="codeheader"><span class="code floatleft">' . $txt['code'] . '</span> <a class="codeoperation smf_select_text">' . $txt['code_select'] . '</a> <a class="codeoperation smf_expand_code hidden" data-shrink-txt="' . $txt['code_shrink'] . '" data-expand-txt="' . $txt['code_expand'] . '">' . $txt['code_expand'] . '</a></div><code class="bbc_code">$1</code>',
1536
				// @todo Maybe this can be simplified?
1537
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1538
				{
1539
					if (!isset($disabled['code']))
1540
					{
1541
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
1542
1543
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
1544
						{
1545
							// Do PHP code coloring?
1546
							if ($php_parts[$php_i] != '&lt;?php')
1547
								continue;
1548
1549
							$php_string = '';
1550
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1551
							{
1552
								$php_string .= $php_parts[$php_i];
1553
								$php_parts[$php_i++] = '';
1554
							}
1555
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1556
						}
1557
1558
						// Fix the PHP code stuff...
1559
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1560
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1561
1562
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1563
						if (!empty($context['browser']['is_opera']))
1564
							$data .= '&nbsp;';
1565
					}
1566
				},
1567
				'block_level' => true,
1568
			),
1569
			array(
1570
				'tag' => 'code',
1571
				'type' => 'unparsed_equals_content',
1572
				'content' => '<div class="codeheader"><span class="code floatleft">' . $txt['code'] . '</span> ($2) <a class="codeoperation smf_select_text">' . $txt['code_select'] . '</a> <a class="codeoperation smf_expand_code hidden" data-shrink-txt="' . $txt['code_shrink'] . '" data-expand-txt="' . $txt['code_expand'] . '">' . $txt['code_expand'] . '</a></div><code class="bbc_code">$1</code>',
1573
				// @todo Maybe this can be simplified?
1574
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1575
				{
1576
					if (!isset($disabled['code']))
1577
					{
1578
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1579
1580
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
1581
						{
1582
							// Do PHP code coloring?
1583
							if ($php_parts[$php_i] != '&lt;?php')
1584
								continue;
1585
1586
							$php_string = '';
1587
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1588
							{
1589
								$php_string .= $php_parts[$php_i];
1590
								$php_parts[$php_i++] = '';
1591
							}
1592
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1593
						}
1594
1595
						// Fix the PHP code stuff...
1596
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1597
						$data[0] = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data[0]);
1598
1599
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1600
						if (!empty($context['browser']['is_opera']))
1601
							$data[0] .= '&nbsp;';
1602
					}
1603
				},
1604
				'block_level' => true,
1605
			),
1606
			array(
1607
				'tag' => 'color',
1608
				'type' => 'unparsed_equals',
1609
				'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]?)\))\]',
1610
				'before' => '<span style="color: $1;" class="bbc_color">',
1611
				'after' => '</span>',
1612
			),
1613
			array(
1614
				'tag' => 'email',
1615
				'type' => 'unparsed_content',
1616
				'content' => '<a href="mailto:$1" class="bbc_email">$1</a>',
1617
				// @todo Should this respect guest_hideContacts?
1618
				'validate' => function(&$tag, &$data, $disabled)
1619
				{
1620
					$data = strtr($data, array('<br>' => ''));
1621
				},
1622
			),
1623
			array(
1624
				'tag' => 'email',
1625
				'type' => 'unparsed_equals',
1626
				'before' => '<a href="mailto:$1" class="bbc_email">',
1627
				'after' => '</a>',
1628
				// @todo Should this respect guest_hideContacts?
1629
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1630
				'disabled_after' => ' ($1)',
1631
			),
1632
			// Legacy (and just a link even when not disabled)
1633
			array(
1634
				'tag' => 'flash',
1635
				'type' => 'unparsed_commas_content',
1636
				'test' => '\d+,\d+\]',
1637
				'content' => '<a href="$1" target="_blank" rel="noopener">$1</a>',
1638
				'validate' => function (&$tag, &$data, $disabled)
1639
				{
1640
					$scheme = parse_url($data[0], PHP_URL_SCHEME);
1641
					if (empty($scheme))
1642
						$data[0] = '//' . ltrim($data[0], ':/');
1643
				},
1644
			),
1645
			array(
1646
				'tag' => 'float',
1647
				'type' => 'unparsed_equals',
1648
				'test' => '(left|right)(\s+max=\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)?\]',
1649
				'before' => '<div $1>',
1650
				'after' => '</div>',
1651
				'validate' => function(&$tag, &$data, $disabled)
1652
				{
1653
					$class = 'class="bbc_float float' . (strpos($data, 'left') === 0 ? 'left' : 'right') . '"';
1654
1655
					if (preg_match('~\bmax=(\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)~', $data, $matches))
1656
						$css = ' style="max-width:' . $matches[1] . (is_numeric($matches[1]) ? 'px' : '') . '"';
1657
					else
1658
						$css = '';
1659
1660
					$data = $class . $css;
1661
				},
1662
				'trim' => 'outside',
1663
				'block_level' => true,
1664
			),
1665
			// Legacy (alias of [url] with an FTP URL)
1666
			array(
1667
				'tag' => 'ftp',
1668
				'type' => 'unparsed_content',
1669
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
1670
				'validate' => function(&$tag, &$data, $disabled)
1671
				{
1672
					$data = strtr($data, array('<br>' => ''));
1673
					$scheme = parse_url($data, PHP_URL_SCHEME);
1674
					if (empty($scheme))
1675
						$data = 'ftp://' . ltrim($data, ':/');
1676
				},
1677
			),
1678
			// Legacy (alias of [url] with an FTP URL)
1679
			array(
1680
				'tag' => 'ftp',
1681
				'type' => 'unparsed_equals',
1682
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
1683
				'after' => '</a>',
1684
				'validate' => function(&$tag, &$data, $disabled)
1685
				{
1686
					$scheme = parse_url($data, PHP_URL_SCHEME);
1687
					if (empty($scheme))
1688
						$data = 'ftp://' . ltrim($data, ':/');
1689
				},
1690
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1691
				'disabled_after' => ' ($1)',
1692
			),
1693
			array(
1694
				'tag' => 'font',
1695
				'type' => 'unparsed_equals',
1696
				'test' => '[A-Za-z0-9_,\-\s]+?\]',
1697
				'before' => '<span style="font-family: $1;" class="bbc_font">',
1698
				'after' => '</span>',
1699
			),
1700
			// Legacy (one of those things that should not be done)
1701
			array(
1702
				'tag' => 'glow',
1703
				'type' => 'unparsed_commas',
1704
				'test' => '[#0-9a-zA-Z\-]{3,12},([012]\d{1,2}|\d{1,2})(,[^]]+)?\]',
1705
				'before' => '<span style="text-shadow: $1 1px 1px 1px">',
1706
				'after' => '</span>',
1707
			),
1708
			// Legacy (alias of [color=green])
1709
			array(
1710
				'tag' => 'green',
1711
				'before' => '<span style="color: green;" class="bbc_color">',
1712
				'after' => '</span>',
1713
			),
1714
			array(
1715
				'tag' => 'html',
1716
				'type' => 'unparsed_content',
1717
				'content' => '<div>$1</div>',
1718
				'block_level' => true,
1719
				'disabled_content' => '$1',
1720
			),
1721
			array(
1722
				'tag' => 'hr',
1723
				'type' => 'closed',
1724
				'content' => '<hr>',
1725
				'block_level' => true,
1726
			),
1727
			array(
1728
				'tag' => 'i',
1729
				'before' => '<i>',
1730
				'after' => '</i>',
1731
			),
1732
			array(
1733
				'tag' => 'img',
1734
				'type' => 'unparsed_content',
1735
				'parameters' => array(
1736
					'alt' => array('optional' => true),
1737
					'title' => array('optional' => true),
1738
					'width' => array('optional' => true, 'value' => ' width="$1"', 'match' => '(\d+)'),
1739
					'height' => array('optional' => true, 'value' => ' height="$1"', 'match' => '(\d+)'),
1740
				),
1741
				'content' => '$1',
1742
				'validate' => function(&$tag, &$data, $disabled, $params)
1743
				{
1744
					$url = strtr($data, array('<br>' => ''));
1745
1746
					if (parse_url($url, PHP_URL_SCHEME) === null)
1747
						$url = '//' . ltrim($url, ':/');
1748
					else
1749
						$url = get_proxied_url($url);
1750
1751
					$alt = !empty($params['{alt}']) ? ' alt="' . $params['{alt}']. '"' : ' alt=""';
1752
					$title = !empty($params['{title}']) ? ' title="' . $params['{title}']. '"' : '';
1753
1754
					$data = isset($disabled[$tag['tag']]) ? $url : '<img src="' . $url . '"' . $alt . $title . $params['{width}'] . $params['{height}'] . ' class="bbc_img' . (!empty($params['{width}']) || !empty($params['{height}']) ? ' resized' : '') . '" loading="lazy">';
1755
				},
1756
				'disabled_content' => '($1)',
1757
			),
1758
			array(
1759
				'tag' => 'iurl',
1760
				'type' => 'unparsed_content',
1761
				'content' => '<a href="$1" class="bbc_link">$1</a>',
1762
				'validate' => function(&$tag, &$data, $disabled)
1763
				{
1764
					$data = strtr($data, array('<br>' => ''));
1765
					$scheme = parse_url($data, PHP_URL_SCHEME);
1766
					if (empty($scheme))
1767
						$data = '//' . ltrim($data, ':/');
1768
				},
1769
			),
1770
			array(
1771
				'tag' => 'iurl',
1772
				'type' => 'unparsed_equals',
1773
				'quoted' => 'optional',
1774
				'before' => '<a href="$1" class="bbc_link">',
1775
				'after' => '</a>',
1776
				'validate' => function(&$tag, &$data, $disabled)
1777
				{
1778
					if (substr($data, 0, 1) == '#')
1779
						$data = '#post_' . substr($data, 1);
1780
					else
1781
					{
1782
						$scheme = parse_url($data, PHP_URL_SCHEME);
1783
						if (empty($scheme))
1784
							$data = '//' . ltrim($data, ':/');
1785
					}
1786
				},
1787
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1788
				'disabled_after' => ' ($1)',
1789
			),
1790
			array(
1791
				'tag' => 'justify',
1792
				'before' => '<div class="justifytext">',
1793
				'after' => '</div>',
1794
				'block_level' => true,
1795
			),
1796
			array(
1797
				'tag' => 'left',
1798
				'before' => '<div class="lefttext">',
1799
				'after' => '</div>',
1800
				'block_level' => true,
1801
			),
1802
			array(
1803
				'tag' => 'li',
1804
				'before' => '<li>',
1805
				'after' => '</li>',
1806
				'trim' => 'outside',
1807
				'require_parents' => array('list'),
1808
				'block_level' => true,
1809
				'disabled_before' => '',
1810
				'disabled_after' => '<br>',
1811
			),
1812
			array(
1813
				'tag' => 'list',
1814
				'before' => '<ul class="bbc_list">',
1815
				'after' => '</ul>',
1816
				'trim' => 'inside',
1817
				'require_children' => array('li', 'list'),
1818
				'block_level' => true,
1819
			),
1820
			array(
1821
				'tag' => 'list',
1822
				'parameters' => array(
1823
					'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)'),
1824
				),
1825
				'before' => '<ul class="bbc_list" style="list-style-type: {type};">',
1826
				'after' => '</ul>',
1827
				'trim' => 'inside',
1828
				'require_children' => array('li'),
1829
				'block_level' => true,
1830
			),
1831
			array(
1832
				'tag' => 'ltr',
1833
				'before' => '<bdo dir="ltr">',
1834
				'after' => '</bdo>',
1835
				'block_level' => true,
1836
			),
1837
			array(
1838
				'tag' => 'me',
1839
				'type' => 'unparsed_equals',
1840
				'before' => '<div class="meaction">* $1 ',
1841
				'after' => '</div>',
1842
				'quoted' => 'optional',
1843
				'block_level' => true,
1844
				'disabled_before' => '/me ',
1845
				'disabled_after' => '<br>',
1846
			),
1847
			array(
1848
				'tag' => 'member',
1849
				'type' => 'unparsed_equals',
1850
				'before' => '<a href="' . $scripturl . '?action=profile;u=$1" class="mention" data-mention="$1">@',
1851
				'after' => '</a>',
1852
			),
1853
			// Legacy (horrible memories of the 1990s)
1854
			array(
1855
				'tag' => 'move',
1856
				'before' => '<marquee>',
1857
				'after' => '</marquee>',
1858
				'block_level' => true,
1859
				'disallow_children' => array('move'),
1860
			),
1861
			array(
1862
				'tag' => 'nobbc',
1863
				'type' => 'unparsed_content',
1864
				'content' => '$1',
1865
			),
1866
			array(
1867
				'tag' => 'php',
1868
				'type' => 'unparsed_content',
1869
				'content' => '<span class="phpcode">$1</span>',
1870
				'validate' => isset($disabled['php']) ? null : function(&$tag, &$data, $disabled)
1871
				{
1872
					if (!isset($disabled['php']))
1873
					{
1874
						$add_begin = substr(trim($data), 0, 5) != '&lt;?';
1875
						$data = highlight_php_code($add_begin ? '&lt;?php ' . $data . '?&gt;' : $data);
1876
						if ($add_begin)
1877
							$data = preg_replace(array('~^(.+?)&lt;\?.{0,40}?php(?:&nbsp;|\s)~', '~\?&gt;((?:</(font|span)>)*)$~'), '$1', $data, 2);
1878
					}
1879
				},
1880
				'block_level' => false,
1881
				'disabled_content' => '$1',
1882
			),
1883
			array(
1884
				'tag' => 'pre',
1885
				'before' => '<pre>',
1886
				'after' => '</pre>',
1887
			),
1888
			array(
1889
				'tag' => 'quote',
1890
				'before' => '<blockquote><cite>' . $txt['quote'] . '</cite>',
1891
				'after' => '</blockquote>',
1892
				'trim' => 'both',
1893
				'block_level' => true,
1894
			),
1895
			array(
1896
				'tag' => 'quote',
1897
				'parameters' => array(
1898
					'author' => array('match' => '(.{1,192}?)', 'quoted' => true),
1899
				),
1900
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1901
				'after' => '</blockquote>',
1902
				'trim' => 'both',
1903
				'block_level' => true,
1904
			),
1905
			array(
1906
				'tag' => 'quote',
1907
				'type' => 'parsed_equals',
1908
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': $1</cite>',
1909
				'after' => '</blockquote>',
1910
				'trim' => 'both',
1911
				'quoted' => 'optional',
1912
				// Don't allow everything to be embedded with the author name.
1913
				'parsed_tags_allowed' => array('url', 'iurl', 'ftp'),
1914
				'block_level' => true,
1915
			),
1916
			array(
1917
				'tag' => 'quote',
1918
				'parameters' => array(
1919
					'author' => array('match' => '([^<>]{1,192}?)'),
1920
					'link' => array('match' => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|msg=\d+?|action=profile;u=\d+)'),
1921
					'date' => array('match' => '(\d+)', 'validate' => 'timeformat'),
1922
				),
1923
				'before' => '<blockquote><cite><a href="' . $scripturl . '?{link}">' . $txt['quote_from'] . ': {author} ' . $txt['search_on'] . ' {date}</a></cite>',
1924
				'after' => '</blockquote>',
1925
				'trim' => 'both',
1926
				'block_level' => true,
1927
			),
1928
			array(
1929
				'tag' => 'quote',
1930
				'parameters' => array(
1931
					'author' => array('match' => '(.{1,192}?)'),
1932
				),
1933
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1934
				'after' => '</blockquote>',
1935
				'trim' => 'both',
1936
				'block_level' => true,
1937
			),
1938
			// Legacy (alias of [color=red])
1939
			array(
1940
				'tag' => 'red',
1941
				'before' => '<span style="color: red;" class="bbc_color">',
1942
				'after' => '</span>',
1943
			),
1944
			array(
1945
				'tag' => 'right',
1946
				'before' => '<div class="righttext">',
1947
				'after' => '</div>',
1948
				'block_level' => true,
1949
			),
1950
			array(
1951
				'tag' => 'rtl',
1952
				'before' => '<bdo dir="rtl">',
1953
				'after' => '</bdo>',
1954
				'block_level' => true,
1955
			),
1956
			array(
1957
				'tag' => 's',
1958
				'before' => '<s>',
1959
				'after' => '</s>',
1960
			),
1961
			// Legacy (never a good idea)
1962
			array(
1963
				'tag' => 'shadow',
1964
				'type' => 'unparsed_commas',
1965
				'test' => '[#0-9a-zA-Z\-]{3,12},(left|right|top|bottom|[0123]\d{0,2})\]',
1966
				'before' => '<span style="text-shadow: $1 $2">',
1967
				'after' => '</span>',
1968
				'validate' => function(&$tag, &$data, $disabled)
1969
				{
1970
1971
					if ($data[1] == 'top' || (is_numeric($data[1]) && $data[1] < 50))
1972
						$data[1] = '0 -2px 1px';
1973
1974
					elseif ($data[1] == 'right' || (is_numeric($data[1]) && $data[1] < 100))
1975
						$data[1] = '2px 0 1px';
1976
1977
					elseif ($data[1] == 'bottom' || (is_numeric($data[1]) && $data[1] < 190))
1978
						$data[1] = '0 2px 1px';
1979
1980
					elseif ($data[1] == 'left' || (is_numeric($data[1]) && $data[1] < 280))
1981
						$data[1] = '-2px 0 1px';
1982
1983
					else
1984
						$data[1] = '1px 1px 1px';
1985
				},
1986
			),
1987
			array(
1988
				'tag' => 'size',
1989
				'type' => 'unparsed_equals',
1990
				'test' => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)\]',
1991
				'before' => '<span style="font-size: $1;" class="bbc_size">',
1992
				'after' => '</span>',
1993
			),
1994
			array(
1995
				'tag' => 'size',
1996
				'type' => 'unparsed_equals',
1997
				'test' => '[1-7]\]',
1998
				'before' => '<span style="font-size: $1;" class="bbc_size">',
1999
				'after' => '</span>',
2000
				'validate' => function(&$tag, &$data, $disabled)
2001
				{
2002
					$sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95);
2003
					$data = $sizes[$data] . 'em';
2004
				},
2005
			),
2006
			array(
2007
				'tag' => 'sub',
2008
				'before' => '<sub>',
2009
				'after' => '</sub>',
2010
			),
2011
			array(
2012
				'tag' => 'sup',
2013
				'before' => '<sup>',
2014
				'after' => '</sup>',
2015
			),
2016
			array(
2017
				'tag' => 'table',
2018
				'before' => '<table class="bbc_table">',
2019
				'after' => '</table>',
2020
				'trim' => 'inside',
2021
				'require_children' => array('tr'),
2022
				'block_level' => true,
2023
			),
2024
			array(
2025
				'tag' => 'td',
2026
				'before' => '<td>',
2027
				'after' => '</td>',
2028
				'require_parents' => array('tr'),
2029
				'trim' => 'outside',
2030
				'block_level' => true,
2031
				'disabled_before' => '',
2032
				'disabled_after' => '',
2033
			),
2034
			array(
2035
				'tag' => 'time',
2036
				'type' => 'unparsed_content',
2037
				'content' => '$1',
2038
				'validate' => function(&$tag, &$data, $disabled)
2039
				{
2040
					if (is_numeric($data))
2041
						$data = timeformat($data);
2042
2043
					$tag['content'] = '<span class="bbc_time">$1</span>';
2044
				},
2045
			),
2046
			array(
2047
				'tag' => 'tr',
2048
				'before' => '<tr>',
2049
				'after' => '</tr>',
2050
				'require_parents' => array('table'),
2051
				'require_children' => array('td'),
2052
				'trim' => 'both',
2053
				'block_level' => true,
2054
				'disabled_before' => '',
2055
				'disabled_after' => '',
2056
			),
2057
			// Legacy (the <tt> element is dead)
2058
			array(
2059
				'tag' => 'tt',
2060
				'before' => '<span class="monospace">',
2061
				'after' => '</span>',
2062
			),
2063
			array(
2064
				'tag' => 'u',
2065
				'before' => '<u>',
2066
				'after' => '</u>',
2067
			),
2068
			array(
2069
				'tag' => 'url',
2070
				'type' => 'unparsed_content',
2071
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
2072
				'validate' => function(&$tag, &$data, $disabled)
2073
				{
2074
					$data = strtr($data, array('<br>' => ''));
2075
					$scheme = parse_url($data, PHP_URL_SCHEME);
2076
					if (empty($scheme))
2077
						$data = '//' . ltrim($data, ':/');
2078
				},
2079
			),
2080
			array(
2081
				'tag' => 'url',
2082
				'type' => 'unparsed_equals',
2083
				'quoted' => 'optional',
2084
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
2085
				'after' => '</a>',
2086
				'validate' => function(&$tag, &$data, $disabled)
2087
				{
2088
					$scheme = parse_url($data, PHP_URL_SCHEME);
2089
					if (empty($scheme))
2090
						$data = '//' . ltrim($data, ':/');
2091
				},
2092
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
2093
				'disabled_after' => ' ($1)',
2094
			),
2095
			// Legacy (alias of [color=white])
2096
			array(
2097
				'tag' => 'white',
2098
				'before' => '<span style="color: white;" class="bbc_color">',
2099
				'after' => '</span>',
2100
			),
2101
			array(
2102
				'tag' => 'youtube',
2103
				'type' => 'unparsed_content',
2104
				'content' => '<div class="videocontainer"><div><iframe frameborder="0" src="https://www.youtube.com/embed/$1?origin=' . $hosturl . '&wmode=opaque" data-youtube-id="$1" allowfullscreen loading="lazy"></iframe></div></div>',
2105
				'disabled_content' => '<a href="https://www.youtube.com/watch?v=$1" target="_blank" rel="noopener">https://www.youtube.com/watch?v=$1</a>',
2106
				'block_level' => true,
2107
			),
2108
		);
2109
2110
		// Inside these tags autolink is not recommendable.
2111
		$no_autolink_tags = array(
2112
			'url',
2113
			'iurl',
2114
			'email',
2115
			'img',
2116
			'html',
2117
		);
2118
2119
		// Let mods add new BBC without hassle.
2120
		call_integration_hook('integrate_bbc_codes', array(&$codes, &$no_autolink_tags));
2121
2122
		// This is mainly for the bbc manager, so it's easy to add tags above.  Custom BBC should be added above this line.
2123
		if ($message === false)
0 ignored issues
show
introduced by
The condition $message === false is always false.
Loading history...
2124
		{
2125
			usort(
2126
				$codes,
2127
				function($a, $b)
2128
				{
2129
					return strcmp($a['tag'], $b['tag']);
2130
				}
2131
			);
2132
			return $codes;
2133
		}
2134
2135
		// So the parser won't skip them.
2136
		$itemcodes = array(
2137
			'*' => 'disc',
2138
			'@' => 'disc',
2139
			'+' => 'square',
2140
			'x' => 'square',
2141
			'#' => 'square',
2142
			'o' => 'circle',
2143
			'O' => 'circle',
2144
			'0' => 'circle',
2145
		);
2146
		if (!isset($disabled['li']) && !isset($disabled['list']))
2147
		{
2148
			foreach ($itemcodes as $c => $dummy)
2149
				$bbc_codes[$c] = array();
2150
		}
2151
2152
		// Shhhh!
2153
		if (!isset($disabled['color']))
2154
		{
2155
			$codes[] = array(
2156
				'tag' => 'chrissy',
2157
				'before' => '<span style="color: #cc0099;">',
2158
				'after' => ' :-*</span>',
2159
			);
2160
			$codes[] = array(
2161
				'tag' => 'kissy',
2162
				'before' => '<span style="color: #cc0099;">',
2163
				'after' => ' :-*</span>',
2164
			);
2165
		}
2166
		$codes[] = array(
2167
			'tag' => 'cowsay',
2168
			'parameters' => array(
2169
				'e' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => 'oo', 'validate' => function ($eyes) use ($smcFunc)
2170
					{
2171
						return $smcFunc['substr']($eyes . 'oo', 0, 2);
2172
					},
2173
				),
2174
				't' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => '  ', 'validate' => function ($tongue) use ($smcFunc)
2175
					{
2176
						return $smcFunc['substr']($tongue . '  ', 0, 2);
2177
					},
2178
				),
2179
			),
2180
			'before' => '<pre data-e="{e}" data-t="{t}"><div>',
2181
			'after' => '</div><script>' . '$("head").append("<style>" + ' . JavaScriptEscape(base64_decode('cHJlW2RhdGEtZV1bZGF0YS10XXt3aGl0ZS1zcGFjZTpwcmUtd3JhcDtsaW5lLWhlaWdodDppbml0aWFsO31wcmVbZGF0YS1lXVtkYXRhLXRdID4gZGl2e2Rpc3BsYXk6dGFibGU7Ym9yZGVyOjFweCBzb2xpZDtib3JkZXItcmFkaXVzOjAuNWVtO3BhZGRpbmc6MWNoO21heC13aWR0aDo4MGNoO21pbi13aWR0aDoxMmNoO31wcmVbZGF0YS1lXVtkYXRhLXRdOjphZnRlcntkaXNwbGF5OmlubGluZS1ibG9jazttYXJnaW4tbGVmdDo4Y2g7bWluLXdpZHRoOjIwY2g7ZGlyZWN0aW9uOmx0cjtjb250ZW50OidcNUMgJycgJycgXl9fXlxBICcnIFw1QyAnJyAoJyBhdHRyKGRhdGEtZSkgJylcNUNfX19fX19fXEEgJycgJycgJycgKF9fKVw1QyAnJyAnJyAnJyAnJyAnJyAnJyAnJyApXDVDL1w1Q1xBICcnICcnICcnICcnICcgYXR0cihkYXRhLXQpICcgfHwtLS0tdyB8XEEgJycgJycgJycgJycgJycgJycgJycgfHwgJycgJycgJycgJycgfHwnO30=')) . ' + "</style>");' . '</script></pre>',
2182
			'block_level' => true,
2183
		);
2184
2185
		foreach ($codes as $code)
2186
		{
2187
			// Make it easier to process parameters later
2188
			if (!empty($code['parameters']))
2189
				ksort($code['parameters'], SORT_STRING);
2190
2191
			// If we are not doing every tag only do ones we are interested in.
2192
			if (empty($parse_tags) || in_array($code['tag'], $parse_tags))
2193
				$bbc_codes[substr($code['tag'], 0, 1)][] = $code;
2194
		}
2195
		$codes = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $codes is dead and can be removed.
Loading history...
2196
	}
2197
2198
	// Shall we take the time to cache this?
2199
	if ($cache_id != '' && !empty($cache_enable) && (($cache_enable >= 2 && isset($message[1000])) || isset($message[2400])) && empty($parse_tags))
2200
	{
2201
		// It's likely this will change if the message is modified.
2202
		$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']);
2203
2204
		if (($temp = cache_get_data($cache_key, 240)) != null)
2205
			return $temp;
2206
2207
		$cache_t = microtime(true);
2208
	}
2209
2210
	if ($smileys === 'print')
0 ignored issues
show
introduced by
The condition $smileys === 'print' is always false.
Loading history...
2211
	{
2212
		// [glow], [shadow], and [move] can't really be printed.
2213
		$disabled['glow'] = true;
2214
		$disabled['shadow'] = true;
2215
		$disabled['move'] = true;
2216
2217
		// Colors can't well be displayed... supposed to be black and white.
2218
		$disabled['color'] = true;
2219
		$disabled['black'] = true;
2220
		$disabled['blue'] = true;
2221
		$disabled['white'] = true;
2222
		$disabled['red'] = true;
2223
		$disabled['green'] = true;
2224
		$disabled['me'] = true;
2225
2226
		// Color coding doesn't make sense.
2227
		$disabled['php'] = true;
2228
2229
		// Links are useless on paper... just show the link.
2230
		$disabled['ftp'] = true;
2231
		$disabled['url'] = true;
2232
		$disabled['iurl'] = true;
2233
		$disabled['email'] = true;
2234
		$disabled['flash'] = true;
2235
2236
		// @todo Change maybe?
2237
		if (!isset($_GET['images']))
2238
		{
2239
			$disabled['img'] = true;
2240
			$disabled['attach'] = true;
2241
		}
2242
2243
		// Maybe some custom BBC need to be disabled for printing.
2244
		call_integration_hook('integrate_bbc_print', array(&$disabled));
2245
	}
2246
2247
	$open_tags = array();
2248
	$message = strtr($message, array("\n" => '<br>'));
2249
2250
	if (!empty($parse_tags))
2251
	{
2252
		$real_alltags_regex = $alltags_regex;
2253
		$alltags_regex = '';
2254
	}
2255
	if (empty($alltags_regex))
2256
	{
2257
		$alltags = array();
2258
		foreach ($bbc_codes as $section)
2259
		{
2260
			foreach ($section as $code)
2261
				$alltags[] = $code['tag'];
2262
		}
2263
		$alltags_regex = '(?' . '>\b' . build_regex(array_unique($alltags)) . '\b|' . build_regex(array_keys($itemcodes)) . ')';
0 ignored issues
show
Bug introduced by
Are you sure build_regex(array_unique($alltags)) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

2263
		$alltags_regex = '(?' . '>\b' . /** @scrutinizer ignore-type */ build_regex(array_unique($alltags)) . '\b|' . build_regex(array_keys($itemcodes)) . ')';
Loading history...
Bug introduced by
Are you sure build_regex(array_keys($itemcodes)) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

2263
		$alltags_regex = '(?' . '>\b' . build_regex(array_unique($alltags)) . '\b|' . /** @scrutinizer ignore-type */ build_regex(array_keys($itemcodes)) . ')';
Loading history...
2264
	}
2265
2266
	$pos = -1;
2267
	while ($pos !== false)
2268
	{
2269
		$last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos;
2270
		preg_match('~\[/?(?=' . $alltags_regex . ')~i', $message, $matches, PREG_OFFSET_CAPTURE, $pos + 1);
2271
		$pos = isset($matches[0][1]) ? $matches[0][1] : false;
2272
2273
		// Failsafe.
2274
		if ($pos === false || $last_pos > $pos)
2275
			$pos = strlen($message) + 1;
2276
2277
		// Can't have a one letter smiley, URL, or email! (Sorry.)
2278
		if ($last_pos < $pos - 1)
2279
		{
2280
			// Make sure the $last_pos is not negative.
2281
			$last_pos = max($last_pos, 0);
2282
2283
			// Pick a block of data to do some raw fixing on.
2284
			$data = substr($message, $last_pos, $pos - $last_pos);
2285
2286
			$placeholders = array();
2287
			$placeholders_counter = 0;
2288
			// Wrap in "private use" Unicode characters to ensure there will be no conflicts.
2289
			$placeholder_template = html_entity_decode('&#xE03C;') . '%1$s' . html_entity_decode('&#xE03E;');
2290
2291
			// Take care of some HTML!
2292
			if (!empty($modSettings['enablePostHTML']) && strpos($data, '&lt;') !== false)
2293
			{
2294
				$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);
2295
2296
				// <br> should be empty.
2297
				$empty_tags = array('br', 'hr');
2298
				foreach ($empty_tags as $tag)
2299
					$data = str_replace(array('&lt;' . $tag . '&gt;', '&lt;' . $tag . '/&gt;', '&lt;' . $tag . ' /&gt;'), '<' . $tag . '>', $data);
2300
2301
				// b, u, i, s, pre... basic tags.
2302
				$closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote', 'strong');
2303
				foreach ($closable_tags as $tag)
2304
				{
2305
					$diff = substr_count($data, '&lt;' . $tag . '&gt;') - substr_count($data, '&lt;/' . $tag . '&gt;');
2306
					$data = strtr($data, array('&lt;' . $tag . '&gt;' => '<' . $tag . '>', '&lt;/' . $tag . '&gt;' => '</' . $tag . '>'));
2307
2308
					if ($diff > 0)
2309
						$data = substr($data, 0, -1) . str_repeat('</' . $tag . '>', $diff) . substr($data, -1);
2310
				}
2311
2312
				// Do <img ...> - with security... action= -> action-.
2313
				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);
2314
				if (!empty($matches[0]))
2315
				{
2316
					$replaces = array();
2317
					foreach ($matches[2] as $match => $imgtag)
2318
					{
2319
						$alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^&quot;|&quot;$~', '', $matches[3][$match]);
2320
2321
						// Remove action= from the URL - no funny business, now.
2322
						// @todo Testing this preg_match seems pointless
2323
						if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) != 0)
2324
							$imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag);
2325
2326
						$placeholder = sprintf($placeholder_template, ++$placeholders_counter);
2327
						$placeholders[$placeholder] = '[img' . $alt . ']' . $imgtag . '[/img]';
2328
2329
						$replaces[$matches[0][$match]] = $placeholder;
2330
					}
2331
2332
					$data = strtr($data, $replaces);
2333
				}
2334
			}
2335
2336
			if (!empty($modSettings['autoLinkUrls']))
2337
			{
2338
				// Are we inside tags that should be auto linked?
2339
				$no_autolink_area = false;
2340
				if (!empty($open_tags))
2341
				{
2342
					foreach ($open_tags as $open_tag)
2343
						if (in_array($open_tag['tag'], $no_autolink_tags))
2344
							$no_autolink_area = true;
2345
				}
2346
2347
				// Don't go backwards.
2348
				// @todo Don't think is the real solution....
2349
				$lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0;
2350
				if ($pos < $lastAutoPos)
2351
					$no_autolink_area = true;
2352
				$lastAutoPos = $pos;
2353
2354
				if (!$no_autolink_area)
2355
				{
2356
					// An &nbsp; right after a URL can break the autolinker
2357
					if (strpos($data, '&nbsp;') !== false)
2358
					{
2359
						$placeholders[sprintf($placeholder_template, 'nbsp')] = '&nbsp;';
2360
						$data = strtr($data, array('&nbsp;' => sprintf($placeholder_template, 'nbsp')));
2361
					}
2362
2363
					// Parse any URLs
2364
					if (!isset($disabled['url']) && strpos($data, '[url') === false)
2365
					{
2366
						// Some reusable character classes
2367
						$space_chars = ($context['utf8'] ? '\p{Z}' : '\s');
2368
						$excluded_trailing_chars = '!;:.,?';
2369
						$domain_label_chars = '0-9A-Za-z\-' . ($context['utf8'] ? implode('', array(
2370
							'\x{A0}-\x{D7FF}', '\x{F900}-\x{FDCF}', '\x{FDF0}-\x{FFEF}',
2371
							'\x{10000}-\x{1FFFD}', '\x{20000}-\x{2FFFD}', '\x{30000}-\x{3FFFD}',
2372
							'\x{40000}-\x{4FFFD}', '\x{50000}-\x{5FFFD}', '\x{60000}-\x{6FFFD}',
2373
							'\x{70000}-\x{7FFFD}', '\x{80000}-\x{8FFFD}', '\x{90000}-\x{9FFFD}',
2374
							'\x{A0000}-\x{AFFFD}', '\x{B0000}-\x{BFFFD}', '\x{C0000}-\x{CFFFD}',
2375
							'\x{D0000}-\x{DFFFD}', '\x{E1000}-\x{EFFFD}',
2376
						)) : '');
2377
2378
						// URI schemes that require some sort of special handling.
2379
						$schemes = array(
2380
							// Schemes whose URI definitions require a domain name in the
2381
							// authority (or whatever the next part of the URI is).
2382
							'need_domain' => array(
2383
								'aaa', 'aaas', 'acap', 'acct', 'afp', 'cap', 'cid', 'coap',
2384
								'coap+tcp', 'coap+ws', 'coaps', 'coaps+tcp', 'coaps+ws', 'crid',
2385
								'cvs', 'dict', 'dns', 'feed', 'fish', 'ftp', 'git', 'go',
2386
								'gopher', 'h323', 'http', 'https', 'iax', 'icap', 'im', 'imap',
2387
								'ipp', 'ipps', 'irc', 'irc6', 'ircs', 'ldap', 'ldaps', 'mailto',
2388
								'mid', 'mupdate', 'nfs', 'nntp', 'pop', 'pres', 'reload',
2389
								'rsync', 'rtsp', 'sftp', 'sieve', 'sip', 'sips', 'smb', 'snmp',
2390
								'soap.beep', 'soap.beeps', 'ssh', 'svn', 'stun', 'stuns',
2391
								'telnet', 'tftp', 'tip', 'tn3270', 'turn', 'turns', 'tv', 'udp',
2392
								'vemmi', 'vnc', 'webcal', 'ws', 'wss', 'xmlrpc.beep',
2393
								'xmlrpc.beeps', 'xmpp', 'z39.50', 'z39.50r', 'z39.50s',
2394
							),
2395
							// Schemes that allow an empty authority ("://" followed by "/")
2396
							'empty_authority' => array(
2397
								'file', 'ni', 'nih',
2398
							),
2399
							// Schemes that do not use an authority but still have a reasonable
2400
							// chance of working as clickable links.
2401
							'no_authority' => array(
2402
								'about', 'callto', 'geo', 'gg', 'leaptofrogans', 'magnet',
2403
								'mailto', 'maps', 'news', 'ni', 'nih', 'service', 'skype',
2404
								'sms', 'tel', 'tv',
2405
							),
2406
							// Schemes that we should never link.
2407
							'forbidden' => array(
2408
								'javascript', 'data',
2409
							),
2410
						);
2411
2412
						// In case a mod wants to control behaviour for a special URI scheme.
2413
						call_integration_hook('integrate_autolinker_schemes', array(&$schemes));
2414
2415
						// Don't repeat this unnecessarily.
2416
						if (empty($url_regex))
2417
						{
2418
							// PCRE subroutines for efficiency.
2419
							$pcre_subroutines = array(
2420
								'tlds' => $modSettings['tld_regex'],
2421
								'pct' => '%[0-9A-Fa-f]{2}',
2422
								'domain_label_char' => '[' . $domain_label_chars . ']',
2423
								'not_domain_label_char' => '[^' . $domain_label_chars . ']',
2424
								'domain' => '(?:(?P>domain_label_char)+\.)+(?P>tlds)',
2425
								'no_domain' => '(?:(?P>domain_label_char)|[._\~!$&\'()*+,;=:@]|(?P>pct))+',
2426
								'scheme_need_domain' => build_regex($schemes['need_domain'], '~'),
2427
								'scheme_empty_authority' => build_regex($schemes['empty_authority'], '~'),
2428
								'scheme_no_authority' => build_regex($schemes['no_authority'], '~'),
2429
								'scheme_any' => '[A-Za-z][0-9A-Za-z+\-.]*',
2430
								'user_info' => '(?:(?P>domain_label_char)|[._\~!$&\'()*+,;=:]|(?P>pct))+',
2431
								'dec_octet' => '(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)',
2432
								'h16' => '[0-9A-Fa-f]{1,4}',
2433
								'ipv4' => '(?:\b(?:(?P>dec_octet)\.){3}(?P>dec_octet)\b)',
2434
								'ipv6' => '\[(?:' . implode('|', array(
2435
									'(?:(?P>h16):){7}(?P>h16)',
2436
									'(?:(?P>h16):){1,7}:',
2437
									'(?:(?P>h16):){1,6}(?::(?P>h16))',
2438
									'(?:(?P>h16):){1,5}(?::(?P>h16)){1,2}',
2439
									'(?:(?P>h16):){1,4}(?::(?P>h16)){1,3}',
2440
									'(?:(?P>h16):){1,3}(?::(?P>h16)){1,4}',
2441
									'(?:(?P>h16):){1,2}(?::(?P>h16)){1,5}',
2442
									'(?P>h16):(?::(?P>h16)){1,6}',
2443
									':(?:(?::(?P>h16)){1,7}|:)',
2444
									'fe80:(?::(?P>h16)){0,4}%[0-9A-Za-z]+',
2445
									'::(ffff(:0{1,4})?:)?(?P>ipv4)',
2446
									'(?:(?P>h16):){1,4}:(?P>ipv4)',
2447
								)) . ')\]',
2448
								'host' => '(?:' . implode('|', array(
2449
									'localhost',
2450
									'(?P>domain)',
2451
									'(?P>ipv4)',
2452
									'(?P>ipv6)',
2453
								)) . ')',
2454
								'authority' => '(?:(?P>user_info)@)?(?P>host)(?::\d+)?',
2455
							);
2456
2457
							// Brackets and quotation marks are problematic at the end of an IRI.
2458
							// E.g.: `http://foo.com/baz(qux)` vs. `(http://foo.com/baz_qux)`
2459
							// In the first case, the user probably intended the `)` as part of the
2460
							// IRI, but not in the second case. To account for this, we test for
2461
							// balanced pairs within the IRI.
2462
							$balanced_pairs = array(
2463
								// Brackets and parentheses
2464
								'(' => ')', '[' => ']', '{' => '}',
2465
								// Double quotation marks
2466
								'"' => '"',
2467
								html_entity_decode('&#x201C;') => html_entity_decode('&#x201D;'),
2468
								html_entity_decode('&#x201E;') => html_entity_decode('&#x201D;'),
2469
								html_entity_decode('&#x201F;') => html_entity_decode('&#x201D;'),
2470
								html_entity_decode('&#x00AB;') => html_entity_decode('&#x00BB;'),
2471
								// Single quotation marks
2472
								'\'' => '\'',
2473
								html_entity_decode('&#x2018;') => html_entity_decode('&#x2019;'),
2474
								html_entity_decode('&#x201A;') => html_entity_decode('&#x2019;'),
2475
								html_entity_decode('&#x201B;') => html_entity_decode('&#x2019;'),
2476
								html_entity_decode('&#x2039;') => html_entity_decode('&#x203A;'),
2477
							);
2478
							foreach ($balanced_pairs as $pair_opener => $pair_closer)
2479
								$balanced_pairs[$smcFunc['htmlspecialchars']($pair_opener)] = $smcFunc['htmlspecialchars']($pair_closer);
2480
2481
							$bracket_quote_chars = '';
2482
							$bracket_quote_entities = array();
2483
							foreach ($balanced_pairs as $pair_opener => $pair_closer)
2484
							{
2485
								if ($pair_opener == $pair_closer)
2486
									$pair_closer = '';
2487
2488
								foreach (array($pair_opener, $pair_closer) as $bracket_quote)
2489
								{
2490
									if (strpos($bracket_quote, '&') === false)
2491
										$bracket_quote_chars .= $bracket_quote;
2492
									else
2493
										$bracket_quote_entities[] = substr($bracket_quote, 1);
2494
								}
2495
							}
2496
							$bracket_quote_chars = str_replace(array('[', ']'), array('\[', '\]'), $bracket_quote_chars);
2497
2498
							$pcre_subroutines['bracket_quote'] = '[' . $bracket_quote_chars . ']|&' . build_regex($bracket_quote_entities, '~');
0 ignored issues
show
Bug introduced by
Are you sure build_regex($bracket_quote_entities, '~') of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

2498
							$pcre_subroutines['bracket_quote'] = '[' . $bracket_quote_chars . ']|&' . /** @scrutinizer ignore-type */ build_regex($bracket_quote_entities, '~');
Loading history...
2499
							$pcre_subroutines['allowed_entities'] = '&(?!' . build_regex(array_merge($bracket_quote_entities, array('lt;', 'gt;')), '~') . ')';
0 ignored issues
show
Bug introduced by
Are you sure build_regex(array_merge(...ay('lt;', 'gt;')), '~') of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

2499
							$pcre_subroutines['allowed_entities'] = '&(?!' . /** @scrutinizer ignore-type */ build_regex(array_merge($bracket_quote_entities, array('lt;', 'gt;')), '~') . ')';
Loading history...
2500
							$pcre_subroutines['excluded_lookahead'] = '(?![' . $excluded_trailing_chars . ']*(?>' . $space_chars . '|<br>|$))';
2501
2502
							foreach (array('path', 'query', 'fragment') as $part)
2503
							{
2504
								switch ($part) {
2505
									case 'path':
2506
										$part_disallowed_chars = '<>' . $bracket_quote_chars . $space_chars . $excluded_trailing_chars . '/#&';
2507
										$part_excluded_trailing_chars = str_replace('?', '', $excluded_trailing_chars);
2508
										break;
2509
2510
									case 'query':
2511
										$part_disallowed_chars = '<>' . $bracket_quote_chars . $space_chars . $excluded_trailing_chars . '#&';
2512
										$part_excluded_trailing_chars = $excluded_trailing_chars;
2513
										break;
2514
2515
									default:
2516
										$part_disallowed_chars = '<>' . $bracket_quote_chars . $space_chars . $excluded_trailing_chars . '&';
2517
										$part_excluded_trailing_chars = $excluded_trailing_chars;
2518
										break;
2519
								}
2520
								$pcre_subroutines[$part . '_allowed'] = '[^' . $part_disallowed_chars . ']|(?P>allowed_entities)|[' . $part_excluded_trailing_chars . '](?P>excluded_lookahead)';
2521
2522
								$balanced_construct_regex = array();
2523
2524
								foreach ($balanced_pairs as $pair_opener => $pair_closer)
2525
									$balanced_construct_regex[] = preg_quote($pair_opener) . '(?P>' . $part . '_recursive)*+' . preg_quote($pair_closer);
2526
2527
								$pcre_subroutines[$part . '_balanced'] = '(?:' . implode('|', $balanced_construct_regex) . ')(?P>' . $part . '_allowed)*+';
2528
								$pcre_subroutines[$part . '_recursive'] = '(?' . '>(?P>' . $part . '_allowed)|(?P>' . $part . '_balanced))';
2529
2530
								$pcre_subroutines[$part . '_segment'] =
2531
									// Allowed characters besides brackets and quotation marks
2532
									'(?P>' . $part . '_allowed)*+' .
2533
									// Brackets and quotation marks that are either...
2534
									'(?:' .
2535
										// part of a balanced construct
2536
										'(?P>' . $part . '_balanced)' .
2537
										// or
2538
										'|' .
2539
										// unpaired but not at the end
2540
										'(?P>bracket_quote)(?=(?P>' . $part . '_allowed))' .
2541
									')*+';
2542
							}
2543
2544
							// Time to build this monster!
2545
							$url_regex =
2546
							// 1. IRI scheme and domain components
2547
							'(?:' .
2548
								// 1a. IRIs with a scheme, or at least an opening "//"
2549
								'(?:' .
2550
2551
									// URI scheme (or lack thereof for schemeless URLs)
2552
									'(?:' .
2553
										// URI scheme and colon
2554
										'\b' .
2555
										'(?:' .
2556
											// Either a scheme that need a domain in the authority
2557
											// (Remember for later that we need a domain)
2558
											'(?P<need_domain>(?P>scheme_need_domain)):' .
2559
											// or
2560
											'|' .
2561
											// a scheme that allows an empty authority
2562
											// (Remember for later that the authority can be empty)
2563
											'(?P<empty_authority>(?P>scheme_empty_authority)):' .
2564
											// or
2565
											'|' .
2566
											// a scheme that uses no authority
2567
											'(?P>scheme_no_authority):(?!//)' .
2568
											// or
2569
											'|' .
2570
											// another scheme, but only if it is followed by "://"
2571
											'(?P>scheme_any):(?=//)' .
2572
										')' .
2573
2574
										// or
2575
										'|' .
2576
2577
										// An empty string followed by "//" for schemeless URLs
2578
										'(?P<schemeless>(?=//))' .
2579
									')' .
2580
2581
									// IRI authority chunk (maybe)
2582
									'(?:' .
2583
										// (Keep track of whether we find a valid authority or not)
2584
										'(?P<has_authority>' .
2585
											// 2 slashes before the authority itself
2586
											'//' .
2587
											'(?:' .
2588
												// If there was no scheme...
2589
												'(?(<schemeless>)' .
2590
													// require an authority that contains a domain.
2591
													'(?P>authority)' .
2592
2593
													// Else if a domain is needed...
2594
													'|(?(<need_domain>)' .
2595
														// require an authority with a domain.
2596
														'(?P>authority)' .
2597
2598
														// Else if an empty authority is allowed...
2599
														'|(?(<empty_authority>)' .
2600
															// then require either
2601
															'(?:' .
2602
																// empty string, followed by a "/"
2603
																'(?=/)' .
2604
																// or
2605
																'|' .
2606
																// an authority with a domain.
2607
																'(?P>authority)' .
2608
															')' .
2609
2610
															// Else just a run of IRI characters.
2611
															'|(?P>no_domain)' .
2612
														')' .
2613
													')' .
2614
												')' .
2615
											')' .
2616
											// Followed by a non-domain character or end of line
2617
											'(?=(?P>not_domain_label_char)|$)' .
2618
										')' .
2619
2620
										// or, if there is a scheme but no authority
2621
										// (e.g. "mailto:" URLs)...
2622
										'|' .
2623
2624
										// A run of IRI characters
2625
										'(?P>no_domain)' .
2626
										// If scheme needs a domain, require a dot and a TLD
2627
										'(?(<need_domain>)\.(?P>tlds))' .
2628
										// Followed by a non-domain character or end of line
2629
										'(?=(?P>not_domain_label_char)|$)' .
2630
									')' .
2631
								')' .
2632
2633
								// Or, if there is neither a scheme nor an authority...
2634
								'|' .
2635
2636
								// 1b. Naked domains
2637
								// (e.g. "example.com" in "Go to example.com for an example.")
2638
								'(?P<naked_domain>' .
2639
									// Preceded by start of line or a space
2640
									'(?<=^|<br>|[' . $space_chars . '])' .
2641
									// A domain name
2642
									'(?P>domain)' .
2643
									// Followed by a non-domain character or end of line
2644
									'(?=(?P>not_domain_label_char)|$)' .
2645
								')' .
2646
							')' .
2647
2648
							// 2. IRI path, query, and fragment components (if present)
2649
							'(?:' .
2650
								// If the IRI has an authority or is a naked domain and any of these
2651
								// components exist, the path must start with a single "/".
2652
								// Note: technically, it is valid to append a query or fragment
2653
								// directly to the authority chunk without a "/", but supporting
2654
								// that in the autolinker would produce a lot of false positives,
2655
								// so we don't.
2656
								'(?=' .
2657
									// If we found an authority above...
2658
									'(?(<has_authority>)' .
2659
										// require a "/"
2660
										'/' .
2661
										// Else if we found a naked domain above...
2662
										'|(?(<naked_domain>)' .
2663
											// require a "/"
2664
											'/' .
2665
										')' .
2666
									')' .
2667
								')' .
2668
2669
								// 2.a. Path component, if any.
2670
								'(?:' .
2671
									// Can have one or more segments
2672
									'(?:' .
2673
										// Not preceded by a "/", except in the special case of an
2674
										// empty authority immediately before the path.
2675
										'(?(<empty_authority>)' .
2676
											'(?:(?<=://)|(?<!/))' .
2677
											'|' .
2678
											'(?<!/)' .
2679
										')' .
2680
										// Initial "/"
2681
										'/' .
2682
										// Then a run of allowed path segement characters
2683
										'(?P>path_segment)*+' .
2684
									')*+' .
2685
								')' .
2686
2687
								// 2.b. Query component, if any.
2688
								'(?:' .
2689
									// Initial "?" that is not last character.
2690
									'\?' . '(?=(?P>bracket_quote)*(?P>query_allowed))' .
2691
									// Then a run of allowed query characters
2692
									'(?P>query_segment)*+' .
2693
								')?' .
2694
2695
								// 2.c. Fragment component, if any.
2696
								'(?:' .
2697
									// Initial "#" that is not last character.
2698
									'#' . '(?=(?P>bracket_quote)*(?P>fragment_allowed))' .
2699
									// Then a run of allowed fragment characters
2700
									'(?P>fragment_segment)*+' .
2701
								')?' .
2702
							')?+';
2703
2704
							// Finally, define the PCRE subroutines in the regex.
2705
							$url_regex .= '(?(DEFINE)';
2706
2707
							foreach ($pcre_subroutines as $name => $subroutine)
2708
								$url_regex .= '(?<' . $name . '>' . $subroutine . ')';
0 ignored issues
show
Bug introduced by
Are you sure $subroutine of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

2708
								$url_regex .= '(?<' . $name . '>' . /** @scrutinizer ignore-type */ $subroutine . ')';
Loading history...
2709
2710
							$url_regex .= ')';
2711
						}
2712
2713
						$tmp_data = preg_replace_callback(
2714
							'~' . $url_regex . '~i' . ($context['utf8'] ? 'u' : ''),
2715
							function($matches) use ($schemes)
2716
							{
2717
								$url = array_shift($matches);
2718
2719
								// If this isn't a clean URL, bail out
2720
								if ($url != sanitize_iri($url))
2721
									return $url;
2722
2723
								$parsedurl = parse_url($url);
2724
2725
								if (!isset($parsedurl['scheme']))
2726
									$parsedurl['scheme'] = '';
2727
2728
								if ($parsedurl['scheme'] == 'mailto')
2729
								{
2730
									if (isset($disabled['email']))
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...
2731
										return $url;
2732
2733
									// Is this version of PHP capable of validating this email address?
2734
									$can_validate = defined('FILTER_FLAG_EMAIL_UNICODE') || strlen($parsedurl['path']) == strspn(strtolower($parsedurl['path']), 'abcdefghijklmnopqrstuvwxyz0123456789!#$%&\'*+-/=?^_`{|}~.@');
2735
2736
									$flags = defined('FILTER_FLAG_EMAIL_UNICODE') ? FILTER_FLAG_EMAIL_UNICODE : null;
2737
2738
									if (!$can_validate || filter_var($parsedurl['path'], FILTER_VALIDATE_EMAIL, $flags) !== false)
0 ignored issues
show
Bug introduced by
It seems like $flags can also be of type null; however, parameter $options of filter_var() does only seem to accept array|integer, maybe add an additional type check? ( Ignorable by Annotation )

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

2738
									if (!$can_validate || filter_var($parsedurl['path'], FILTER_VALIDATE_EMAIL, /** @scrutinizer ignore-type */ $flags) !== false)
Loading history...
2739
										return '[email=' . str_replace('mailto:', '', $url) . ']' . $url . '[/email]';
2740
									else
2741
										return $url;
2742
								}
2743
2744
								// Are we linking a schemeless URL or naked domain name (e.g. "example.com")?
2745
								if (empty($parsedurl['scheme']))
2746
									$fullUrl = '//' . ltrim($url, ':/');
2747
								else
2748
									$fullUrl = $url;
2749
2750
								// Make sure that $fullUrl really is valid
2751
								if (in_array($parsedurl['scheme'], $schemes['forbidden']) || (!in_array($parsedurl['scheme'], $schemes['no_authority']) && validate_iri((strpos($fullUrl, '//') === 0 ? 'http:' : '') . $fullUrl) === false))
2752
									return $url;
2753
2754
								return '[url=&quot;' . str_replace(array('[', ']'), array('&#91;', '&#93;'), $fullUrl) . '&quot;]' . $url . '[/url]';
2755
							},
2756
							$data
2757
						);
2758
2759
						if (!is_null($tmp_data))
2760
							$data = $tmp_data;
2761
					}
2762
2763
					// Next, emails...  Must be careful not to step on enablePostHTML logic above...
2764
					if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false && stripos($data, 'mailto:') === false)
2765
					{
2766
						// Preceded by a space or start of line
2767
						$email_regex = '(?<=^|<br>|[' . $space_chars . '])' .
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $space_chars does not seem to be defined for all execution paths leading up to this point.
Loading history...
2768
2769
						// An email address
2770
						'[' . $domain_label_chars . '_.]{1,80}' .
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $domain_label_chars does not seem to be defined for all execution paths leading up to this point.
Loading history...
2771
						'@' .
2772
						'[' . $domain_label_chars . '.]+' .
2773
						'\.' . $modSettings['tld_regex'] .
2774
2775
						// Followed by a non-domain character or end of line
2776
						'(?=[^' . $domain_label_chars . ']|$)';
2777
2778
						$tmp_data = preg_replace('~' . $email_regex . '~i' . ($context['utf8'] ? 'u' : ''), '[email]$0[/email]', $data);
2779
2780
						if (!is_null($tmp_data))
2781
							$data = $tmp_data;
2782
					}
2783
2784
					// Save a little memory.
2785
					unset($tmp_data);
2786
				}
2787
			}
2788
2789
			// Restore any placeholders
2790
			$data = strtr($data, $placeholders);
2791
2792
			$data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));
2793
2794
			// If it wasn't changed, no copying or other boring stuff has to happen!
2795
			if ($data != substr($message, $last_pos, $pos - $last_pos))
2796
			{
2797
				$message = substr($message, 0, $last_pos) . $data . substr($message, $pos);
2798
2799
				// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
2800
				$old_pos = strlen($data) + $last_pos;
2801
				$pos = strpos($message, '[', $last_pos);
2802
				$pos = $pos === false ? $old_pos : min($pos, $old_pos);
2803
			}
2804
		}
2805
2806
		// Are we there yet?  Are we there yet?
2807
		if ($pos >= strlen($message) - 1)
2808
			break;
2809
2810
		$tag_character = strtolower($message[$pos + 1]);
2811
2812
		if ($tag_character == '/' && !empty($open_tags))
2813
		{
2814
			$pos2 = strpos($message, ']', $pos + 1);
2815
			if ($pos2 == $pos + 2)
2816
				continue;
2817
2818
			$look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));
2819
2820
			// A closing tag that doesn't match any open tags? Skip it.
2821
			if (!in_array($look_for, array_map(function($code) { return $code['tag']; }, $open_tags)))
2822
				continue;
2823
2824
			$to_close = array();
2825
			$block_level = null;
2826
2827
			do
2828
			{
2829
				$tag = array_pop($open_tags);
2830
				if (!$tag)
2831
					break;
2832
2833
				if (!empty($tag['block_level']))
2834
				{
2835
					// Only find out if we need to.
2836
					if ($block_level === false)
2837
					{
2838
						array_push($open_tags, $tag);
2839
						break;
2840
					}
2841
2842
					// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
2843
					if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]]))
2844
					{
2845
						foreach ($bbc_codes[$look_for[0]] as $temp)
2846
							if ($temp['tag'] == $look_for)
2847
							{
2848
								$block_level = !empty($temp['block_level']);
2849
								break;
2850
							}
2851
					}
2852
2853
					if ($block_level !== true)
2854
					{
2855
						$block_level = false;
2856
						array_push($open_tags, $tag);
2857
						break;
2858
					}
2859
				}
2860
2861
				$to_close[] = $tag;
2862
			}
2863
			while ($tag['tag'] != $look_for);
2864
2865
			// Did we just eat through everything and not find it?
2866
			if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
2867
			{
2868
				$open_tags = $to_close;
2869
				continue;
2870
			}
2871
			elseif (!empty($to_close) && $tag['tag'] != $look_for)
2872
			{
2873
				if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]]))
2874
				{
2875
					foreach ($bbc_codes[$look_for[0]] as $temp)
2876
						if ($temp['tag'] == $look_for)
2877
						{
2878
							$block_level = !empty($temp['block_level']);
2879
							break;
2880
						}
2881
				}
2882
2883
				// We're not looking for a block level tag (or maybe even a tag that exists...)
2884
				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...
2885
				{
2886
					foreach ($to_close as $tag)
2887
						array_push($open_tags, $tag);
2888
					continue;
2889
				}
2890
			}
2891
2892
			foreach ($to_close as $tag)
2893
			{
2894
				$message = substr($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1);
2895
				$pos += strlen($tag['after']) + 2;
2896
				$pos2 = $pos - 1;
2897
2898
				// See the comment at the end of the big loop - just eating whitespace ;).
2899
				$whitespace_regex = '';
2900
				if (!empty($tag['block_level']))
2901
					$whitespace_regex .= '(&nbsp;|\s)*(<br\s*/?' . '>)?';
2902
				// Trim one line of whitespace after unnested tags, but all of it after nested ones
2903
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
2904
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2905
2906
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
2907
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2908
			}
2909
2910
			if (!empty($to_close))
2911
			{
2912
				$to_close = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $to_close is dead and can be removed.
Loading history...
2913
				$pos--;
2914
			}
2915
2916
			continue;
2917
		}
2918
2919
		// No tags for this character, so just keep going (fastest possible course.)
2920
		if (!isset($bbc_codes[$tag_character]))
2921
			continue;
2922
2923
		$inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
2924
		$tag = null;
2925
		foreach ($bbc_codes[$tag_character] as $possible)
2926
		{
2927
			$pt_strlen = strlen($possible['tag']);
2928
2929
			// Not a match?
2930
			if (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag'])
2931
				continue;
2932
2933
			$next_c = isset($message[$pos + 1 + $pt_strlen]) ? $message[$pos + 1 + $pt_strlen] : '';
2934
2935
			// A tag is the last char maybe
2936
			if ($next_c == '')
2937
				break;
2938
2939
			// A test validation?
2940
			if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0)
2941
				continue;
2942
			// Do we want parameters?
2943
			elseif (!empty($possible['parameters']))
2944
			{
2945
				// Are all the parameters optional?
2946
				$param_required = false;
2947
				foreach ($possible['parameters'] as $param)
2948
				{
2949
					if (empty($param['optional']))
2950
					{
2951
						$param_required = true;
2952
						break;
2953
					}
2954
				}
2955
2956
				if ($param_required && $next_c != ' ')
2957
					continue;
2958
			}
2959
			elseif (isset($possible['type']))
2960
			{
2961
				// Do we need an equal sign?
2962
				if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=')
2963
					continue;
2964
				// Maybe we just want a /...
2965
				if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + $pt_strlen, 2) != '/]' && substr($message, $pos + 1 + $pt_strlen, 3) != ' /]')
2966
					continue;
2967
				// An immediate ]?
2968
				if ($possible['type'] == 'unparsed_content' && $next_c != ']')
2969
					continue;
2970
			}
2971
			// No type means 'parsed_content', which demands an immediate ] without parameters!
2972
			elseif ($next_c != ']')
2973
				continue;
2974
2975
			// Check allowed tree?
2976
			if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
2977
				continue;
2978
			elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
2979
				continue;
2980
			// If this is in the list of disallowed child tags, don't parse it.
2981
			elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
2982
				continue;
2983
2984
			$pos1 = $pos + 1 + $pt_strlen + 1;
2985
2986
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
2987
			if ($possible['tag'] == 'quote')
2988
			{
2989
				// Start with standard
2990
				$quote_alt = false;
2991
				foreach ($open_tags as $open_quote)
2992
				{
2993
					// Every parent quote this quote has flips the styling
2994
					if ($open_quote['tag'] == 'quote')
2995
						$quote_alt = !$quote_alt;
0 ignored issues
show
introduced by
The condition $quote_alt is always false.
Loading history...
2996
				}
2997
				// Add a class to the quote to style alternating blockquotes
2998
				$possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">'));
2999
			}
3000
3001
			// This is long, but it makes things much easier and cleaner.
3002
			if (!empty($possible['parameters']))
3003
			{
3004
				// Build a regular expression for each parameter for the current tag.
3005
				$regex_key = $smcFunc['json_encode']($possible['parameters']);
3006
				if (!isset($params_regexes[$regex_key]))
3007
				{
3008
					$params_regexes[$regex_key] = '';
3009
3010
					foreach ($possible['parameters'] as $p => $info)
3011
						$params_regexes[$regex_key] .= '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . '\s*)' . (empty($info['optional']) ? '' : '?');
3012
				}
3013
3014
				// Extract the string that potentially holds our parameters.
3015
				$blob = preg_split('~\[/?(?:' . $alltags_regex . ')~i', substr($message, $pos));
3016
				$blobs = preg_split('~\]~i', $blob[1]);
3017
3018
				$splitters = implode('=|', array_keys($possible['parameters'])) . '=';
3019
3020
				// Progressively append more blobs until we find our parameters or run out of blobs
3021
				$blob_counter = 1;
3022
				while ($blob_counter <= count($blobs))
3023
				{
3024
					$given_param_string = implode(']', array_slice($blobs, 0, $blob_counter++));
3025
3026
					$given_params = preg_split('~\s(?=(' . $splitters . '))~i', $given_param_string);
3027
					sort($given_params, SORT_STRING);
3028
3029
					$match = preg_match('~^' . $params_regexes[$regex_key] . '$~i', implode(' ', $given_params), $matches) !== 0;
3030
3031
					if ($match)
3032
						break;
3033
				}
3034
3035
				// Didn't match our parameter list, try the next possible.
3036
				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...
3037
					continue;
3038
3039
				$params = array();
3040
				for ($i = 1, $n = count($matches); $i < $n; $i += 2)
3041
				{
3042
					$key = strtok(ltrim($matches[$i]), '=');
3043
					if ($key === false)
3044
						continue;
3045
					elseif (isset($possible['parameters'][$key]['value']))
3046
						$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
3047
					elseif (isset($possible['parameters'][$key]['validate']))
3048
						$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
3049
					else
3050
						$params['{' . $key . '}'] = $matches[$i + 1];
3051
3052
					// Just to make sure: replace any $ or { so they can't interpolate wrongly.
3053
					$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
3054
				}
3055
3056
				foreach ($possible['parameters'] as $p => $info)
3057
				{
3058
					if (!isset($params['{' . $p . '}']))
3059
					{
3060
						if (!isset($info['default']))
3061
							$params['{' . $p . '}'] = '';
3062
						elseif (isset($possible['parameters'][$p]['value']))
3063
							$params['{' . $p . '}'] = strtr($possible['parameters'][$p]['value'], array('$1' => $info['default']));
3064
						elseif (isset($possible['parameters'][$p]['validate']))
3065
							$params['{' . $p . '}'] = $possible['parameters'][$p]['validate']($info['default']);
3066
						else
3067
							$params['{' . $p . '}'] = $info['default'];
3068
					}
3069
				}
3070
3071
				$tag = $possible;
3072
3073
				// Put the parameters into the string.
3074
				if (isset($tag['before']))
3075
					$tag['before'] = strtr($tag['before'], $params);
3076
				if (isset($tag['after']))
3077
					$tag['after'] = strtr($tag['after'], $params);
3078
				if (isset($tag['content']))
3079
					$tag['content'] = strtr($tag['content'], $params);
3080
3081
				$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...
3082
			}
3083
			else
3084
			{
3085
				$tag = $possible;
3086
				$params = array();
3087
			}
3088
			break;
3089
		}
3090
3091
		// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
3092
		if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
3093
		{
3094
			if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
3095
				continue;
3096
3097
			$tag = $itemcodes[$message[$pos + 1]];
3098
3099
			// First let's set up the tree: it needs to be in a list, or after an li.
3100
			if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
3101
			{
3102
				$open_tags[] = array(
3103
					'tag' => 'list',
3104
					'after' => '</ul>',
3105
					'block_level' => true,
3106
					'require_children' => array('li'),
3107
					'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
3108
				);
3109
				$code = '<ul class="bbc_list">';
3110
			}
3111
			// We're in a list item already: another itemcode?  Close it first.
3112
			elseif ($inside['tag'] == 'li')
3113
			{
3114
				array_pop($open_tags);
3115
				$code = '</li>';
3116
			}
3117
			else
3118
				$code = '';
3119
3120
			// Now we open a new tag.
3121
			$open_tags[] = array(
3122
				'tag' => 'li',
3123
				'after' => '</li>',
3124
				'trim' => 'outside',
3125
				'block_level' => true,
3126
				'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
3127
			);
3128
3129
			// First, open the tag...
3130
			$code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
3131
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
3132
			$pos += strlen($code) - 1 + 2;
3133
3134
			// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
3135
			$pos2 = strpos($message, '<br>', $pos);
3136
			$pos3 = strpos($message, '[/', $pos);
3137
			if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
3138
			{
3139
				preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 4), $matches);
3140
				$message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
3141
3142
				$open_tags[count($open_tags) - 2]['after'] = '</ul>';
3143
			}
3144
			// Tell the [list] that it needs to close specially.
3145
			else
3146
			{
3147
				// Move the li over, because we're not sure what we'll hit.
3148
				$open_tags[count($open_tags) - 1]['after'] = '';
3149
				$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
3150
			}
3151
3152
			continue;
3153
		}
3154
3155
		// Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
3156
		if ($tag === null && $inside !== null && !empty($inside['require_children']))
3157
		{
3158
			array_pop($open_tags);
3159
3160
			$message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
3161
			$pos += strlen($inside['after']) - 1 + 2;
3162
		}
3163
3164
		// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
3165
		if ($tag === null)
3166
			continue;
3167
3168
		// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
3169
		if (isset($inside['disallow_children']))
3170
			$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
3171
3172
		// Is this tag disabled?
3173
		if (isset($disabled[$tag['tag']]))
3174
		{
3175
			if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
3176
			{
3177
				$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
3178
				$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
3179
				$tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
3180
			}
3181
			elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
3182
			{
3183
				$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
3184
				$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
3185
			}
3186
			else
3187
				$tag['content'] = $tag['disabled_content'];
3188
		}
3189
3190
		// we use this a lot
3191
		$tag_strlen = strlen($tag['tag']);
3192
3193
		// The only special case is 'html', which doesn't need to close things.
3194
		if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
3195
		{
3196
			$n = count($open_tags) - 1;
3197
			while (empty($open_tags[$n]['block_level']) && $n >= 0)
3198
				$n--;
3199
3200
			// Close all the non block level tags so this tag isn't surrounded by them.
3201
			for ($i = count($open_tags) - 1; $i > $n; $i--)
3202
			{
3203
				$message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
3204
				$ot_strlen = strlen($open_tags[$i]['after']);
3205
				$pos += $ot_strlen + 2;
3206
				$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...
3207
3208
				// Trim or eat trailing stuff... see comment at the end of the big loop.
3209
				$whitespace_regex = '';
3210
				if (!empty($tag['block_level']))
3211
					$whitespace_regex .= '(&nbsp;|\s)*(<br>)?';
3212
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
3213
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
3214
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
3215
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
3216
3217
				array_pop($open_tags);
3218
			}
3219
		}
3220
3221
		// Can't read past the end of the message
3222
		$pos1 = min(strlen($message), $pos1);
3223
3224
		// No type means 'parsed_content'.
3225
		if (!isset($tag['type']))
3226
		{
3227
			$open_tags[] = $tag;
3228
3229
			// There's no data to change, but maybe do something based on params?
3230
			$data = null;
3231
			if (isset($tag['validate']))
3232
				$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...
3233
3234
			$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
3235
			$pos += strlen($tag['before']) - 1 + 2;
3236
		}
3237
		// Don't parse the content, just skip it.
3238
		elseif ($tag['type'] == 'unparsed_content')
3239
		{
3240
			$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
3241
			if ($pos2 === false)
3242
				continue;
3243
3244
			$data = substr($message, $pos1, $pos2 - $pos1);
3245
3246
			if (!empty($tag['block_level']) && substr($data, 0, 4) == '<br>')
3247
				$data = substr($data, 4);
3248
3249
			if (isset($tag['validate']))
3250
				$tag['validate']($tag, $data, $disabled, $params);
3251
3252
			$code = strtr($tag['content'], array('$1' => $data));
3253
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
3254
3255
			$pos += strlen($code) - 1 + 2;
3256
			$last_pos = $pos + 1;
3257
		}
3258
		// Don't parse the content, just skip it.
3259
		elseif ($tag['type'] == 'unparsed_equals_content')
3260
		{
3261
			// The value may be quoted for some tags - check.
3262
			if (isset($tag['quoted']))
3263
			{
3264
				$quoted = substr($message, $pos1, 6) == '&quot;';
3265
				if ($tag['quoted'] != 'optional' && !$quoted)
3266
					continue;
3267
3268
				if ($quoted)
3269
					$pos1 += 6;
3270
			}
3271
			else
3272
				$quoted = false;
3273
3274
			$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...
3275
			if ($pos2 === false)
3276
				continue;
3277
3278
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3279
			if ($pos3 === false)
3280
				continue;
3281
3282
			$data = array(
3283
				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...
3284
				substr($message, $pos1, $pos2 - $pos1)
3285
			);
3286
3287
			if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '<br>')
3288
				$data[0] = substr($data[0], 4);
3289
3290
			// Validation for my parking, please!
3291
			if (isset($tag['validate']))
3292
				$tag['validate']($tag, $data, $disabled, $params);
3293
3294
			$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
3295
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3296
			$pos += strlen($code) - 1 + 2;
3297
		}
3298
		// A closed tag, with no content or value.
3299
		elseif ($tag['type'] == 'closed')
3300
		{
3301
			$pos2 = strpos($message, ']', $pos);
3302
3303
			// Maybe a custom BBC wants to do something special?
3304
			$data = null;
3305
			if (isset($tag['validate']))
3306
				$tag['validate']($tag, $data, $disabled, $params);
3307
3308
			$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
3309
			$pos += strlen($tag['content']) - 1 + 2;
3310
		}
3311
		// This one is sorta ugly... :/.  Unfortunately, it's needed for flash.
3312
		elseif ($tag['type'] == 'unparsed_commas_content')
3313
		{
3314
			$pos2 = strpos($message, ']', $pos1);
3315
			if ($pos2 === false)
3316
				continue;
3317
3318
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3319
			if ($pos3 === false)
3320
				continue;
3321
3322
			// We want $1 to be the content, and the rest to be csv.
3323
			$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
3324
			$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
3325
3326
			if (isset($tag['validate']))
3327
				$tag['validate']($tag, $data, $disabled, $params);
3328
3329
			$code = $tag['content'];
3330
			foreach ($data as $k => $d)
3331
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3332
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3333
			$pos += strlen($code) - 1 + 2;
3334
		}
3335
		// This has parsed content, and a csv value which is unparsed.
3336
		elseif ($tag['type'] == 'unparsed_commas')
3337
		{
3338
			$pos2 = strpos($message, ']', $pos1);
3339
			if ($pos2 === false)
3340
				continue;
3341
3342
			$data = explode(',', substr($message, $pos1, $pos2 - $pos1));
3343
3344
			if (isset($tag['validate']))
3345
				$tag['validate']($tag, $data, $disabled, $params);
3346
3347
			// Fix after, for disabled code mainly.
3348
			foreach ($data as $k => $d)
3349
				$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
3350
3351
			$open_tags[] = $tag;
3352
3353
			// Replace them out, $1, $2, $3, $4, etc.
3354
			$code = $tag['before'];
3355
			foreach ($data as $k => $d)
3356
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3357
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1);
3358
			$pos += strlen($code) - 1 + 2;
3359
		}
3360
		// A tag set to a value, parsed or not.
3361
		elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
3362
		{
3363
			// The value may be quoted for some tags - check.
3364
			if (isset($tag['quoted']))
3365
			{
3366
				$quoted = substr($message, $pos1, 6) == '&quot;';
3367
				if ($tag['quoted'] != 'optional' && !$quoted)
3368
					continue;
3369
3370
				if ($quoted)
3371
					$pos1 += 6;
3372
			}
3373
			else
3374
				$quoted = false;
3375
3376
			if ($quoted)
3377
			{
3378
				$end_of_value = strpos($message, '&quot;]', $pos1);
3379
				$nested_tag = strpos($message, '=&quot;', $pos1);
3380
				// Check so this is not just an quoted url ending with a =
3381
				if ($nested_tag && substr($message, $nested_tag, 8) == '=&quot;]')
3382
					$nested_tag = false;
3383
				if ($nested_tag && $nested_tag < $end_of_value)
0 ignored issues
show
Bug Best Practice introduced by
The expression $nested_tag of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
3384
					// Nested tag with quoted value detected, use next end tag
3385
					$nested_tag_pos = strpos($message, $quoted == false ? ']' : '&quot;]', $pos1) + 6;
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

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

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

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

Loading history...
3389
			if ($pos2 === false)
3390
				continue;
3391
3392
			$data = substr($message, $pos1, $pos2 - $pos1);
3393
3394
			// Validation for my parking, please!
3395
			if (isset($tag['validate']))
3396
				$tag['validate']($tag, $data, $disabled, $params);
3397
3398
			// For parsed content, we must recurse to avoid security problems.
3399
			if ($tag['type'] != 'unparsed_equals')
3400
				$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
3401
3402
			$tag['after'] = strtr($tag['after'], array('$1' => $data));
3403
3404
			$open_tags[] = $tag;
3405
3406
			$code = strtr($tag['before'], array('$1' => $data));
3407
			$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...
3408
			$pos += strlen($code) - 1 + 2;
3409
		}
3410
3411
		// If this is block level, eat any breaks after it.
3412
		if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '<br>')
3413
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 5);
3414
3415
		// Are we trimming outside this tag?
3416
		if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
3417
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
3418
	}
3419
3420
	// Close any remaining tags.
3421
	while ($tag = array_pop($open_tags))
3422
		$message .= "\n" . $tag['after'] . "\n";
3423
3424
	// Parse the smileys within the parts where it can be done safely.
3425
	if ($smileys === true)
3426
	{
3427
		$message_parts = explode("\n", $message);
3428
		for ($i = 0, $n = count($message_parts); $i < $n; $i += 2)
3429
			parsesmileys($message_parts[$i]);
3430
3431
		$message = implode('', $message_parts);
3432
	}
3433
3434
	// No smileys, just get rid of the markers.
3435
	else
3436
		$message = strtr($message, array("\n" => ''));
3437
3438
	if ($message !== '' && $message[0] === ' ')
3439
		$message = '&nbsp;' . substr($message, 1);
3440
3441
	// Cleanup whitespace.
3442
	$message = strtr($message, array('  ' => ' &nbsp;', "\r" => '', "\n" => '<br>', '<br> ' => '<br>&nbsp;', '&#13;' => "\n"));
3443
3444
	// Allow mods access to what parse_bbc created
3445
	call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
3446
3447
	// Cache the output if it took some time...
3448
	if (isset($cache_key, $cache_t) && microtime(true) - $cache_t > 0.05)
3449
		cache_put_data($cache_key, $message, 240);
3450
3451
	// If this was a force parse revert if needed.
3452
	if (!empty($parse_tags))
3453
	{
3454
		$alltags_regex = empty($real_alltags_regex) ? '' : $real_alltags_regex;
3455
		unset($real_alltags_regex);
3456
	}
3457
	elseif (!empty($bbc_codes))
3458
		$bbc_lang_locales[$txt['lang_locale']] = $bbc_codes;
3459
3460
	return $message;
3461
}
3462
3463
/**
3464
 * Parse smileys in the passed message.
3465
 *
3466
 * The smiley parsing function which makes pretty faces appear :).
3467
 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
3468
 * These are specifically not parsed in code tags [url=mailto:[email protected]]
3469
 * Caches the smileys from the database or array in memory.
3470
 * Doesn't return anything, but rather modifies message directly.
3471
 *
3472
 * @param string &$message The message to parse smileys in
3473
 */
3474
function parsesmileys(&$message)
3475
{
3476
	global $modSettings, $txt, $user_info, $context, $smcFunc;
3477
	static $smileyPregSearch = null, $smileyPregReplacements = array();
3478
3479
	// No smiley set at all?!
3480
	if ($user_info['smiley_set'] == 'none' || trim($message) == '')
3481
		return;
3482
3483
	// Maybe a mod wants to implement an alternative method (e.g. emojis instead of images)
3484
	call_integration_hook('integrate_smileys', array(&$smileyPregSearch, &$smileyPregReplacements));
3485
3486
	// If smileyPregSearch hasn't been set, do it now.
3487
	if (empty($smileyPregSearch))
3488
	{
3489
		// Cache for longer when customized smiley codes aren't enabled
3490
		$cache_time = empty($modSettings['smiley_enable']) ? 7200 : 480;
3491
3492
		// Load the smileys in reverse order by length so they don't get parsed incorrectly.
3493
		if (($temp = cache_get_data('parsing_smileys_' . $user_info['smiley_set'], $cache_time)) == null)
3494
		{
3495
			$result = $smcFunc['db_query']('', '
3496
				SELECT s.code, f.filename, s.description
3497
				FROM {db_prefix}smileys AS s
3498
					JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
3499
				WHERE f.smiley_set = {string:smiley_set}' . (empty($modSettings['smiley_enable']) ? '
3500
					AND s.code IN ({array_string:default_codes})' : '') . '
3501
				ORDER BY LENGTH(s.code) DESC',
3502
				array(
3503
					'default_codes' => array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'),
3504
					'smiley_set' => $user_info['smiley_set'],
3505
				)
3506
			);
3507
			$smileysfrom = array();
3508
			$smileysto = array();
3509
			$smileysdescs = array();
3510
			while ($row = $smcFunc['db_fetch_assoc']($result))
3511
			{
3512
				$smileysfrom[] = $row['code'];
3513
				$smileysto[] = $smcFunc['htmlspecialchars']($row['filename']);
3514
				$smileysdescs[] = !empty($txt['icon_' . strtolower($row['description'])]) ? $txt['icon_' . strtolower($row['description'])] : $row['description'];
3515
			}
3516
			$smcFunc['db_free_result']($result);
3517
3518
			cache_put_data('parsing_smileys_' . $user_info['smiley_set'], array($smileysfrom, $smileysto, $smileysdescs), $cache_time);
3519
		}
3520
		else
3521
			list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
3522
3523
		// The non-breaking-space is a complex thing...
3524
		$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
3525
3526
		// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:[email protected]] doesn't parse the :D smiley)
3527
		$smileyPregReplacements = array();
3528
		$searchParts = array();
3529
		$smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
3530
3531
		for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
3532
		{
3533
			$specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES);
3534
			$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">';
3535
3536
			$smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
3537
3538
			$searchParts[] = $smileysfrom[$i];
3539
			if ($smileysfrom[$i] != $specialChars)
3540
			{
3541
				$smileyPregReplacements[$specialChars] = $smileyCode;
3542
				$searchParts[] = $specialChars;
3543
3544
				// Some 2.0 hex htmlchars are in there as 3 digits; allow for finding leading 0 or not
3545
				$specialChars2 = preg_replace('/&#(\d{2});/', '&#0$1;', $specialChars);
3546
				if ($specialChars2 != $specialChars)
3547
				{
3548
					$smileyPregReplacements[$specialChars2] = $smileyCode;
3549
					$searchParts[] = $specialChars2;
3550
				}
3551
			}
3552
		}
3553
3554
		$smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?<![a-zA-Z0-9])\(|^)(' . build_regex($searchParts, '~') . ')(?=[^[:alpha:]0-9]|$)~' . ($context['utf8'] ? 'u' : '');
0 ignored issues
show
Bug introduced by
Are you sure build_regex($searchParts, '~') of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

3554
		$smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?<![a-zA-Z0-9])\(|^)(' . /** @scrutinizer ignore-type */ build_regex($searchParts, '~') . ')(?=[^[:alpha:]0-9]|$)~' . ($context['utf8'] ? 'u' : '');
Loading history...
3555
	}
3556
3557
	// If there are no smileys defined, no need to replace anything
3558
	if (empty($smileyPregReplacements))
3559
		return;
3560
3561
	// Replace away!
3562
	$message = preg_replace_callback(
3563
		$smileyPregSearch,
3564
		function($matches) use ($smileyPregReplacements)
3565
		{
3566
			return $smileyPregReplacements[$matches[1]];
3567
		},
3568
		$message
3569
	);
3570
}
3571
3572
/**
3573
 * Highlight any code.
3574
 *
3575
 * Uses PHP's highlight_string() to highlight PHP syntax
3576
 * does special handling to keep the tabs in the code available.
3577
 * used to parse PHP code from inside [code] and [php] tags.
3578
 *
3579
 * @param string $code The code
3580
 * @return string The code with highlighted HTML.
3581
 */
3582
function highlight_php_code($code)
3583
{
3584
	// Remove special characters.
3585
	$code = un_htmlspecialchars(strtr($code, array('<br />' => "\n", '<br>' => "\n", "\t" => 'SMF_TAB();', '&#91;' => '[')));
3586
3587
	$oldlevel = error_reporting(0);
3588
3589
	$buffer = str_replace(array("\n", "\r"), '', @highlight_string($code, true));
3590
3591
	error_reporting($oldlevel);
3592
3593
	// Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
3594
	$buffer = preg_replace('~SMF_TAB(?:</(?:font|span)><(?:font color|span style)="[^"]*?">)?\\(\\);~', '<pre style="display: inline;">' . "\t" . '</pre>', $buffer);
3595
3596
	return strtr($buffer, array('\'' => '&#039;', '<code>' => '', '</code>' => ''));
3597
}
3598
3599
/**
3600
 * Gets the appropriate URL to use for images (or whatever) when using SSL
3601
 *
3602
 * The returned URL may or may not be a proxied URL, depending on the situation.
3603
 * Mods can implement alternative proxies using the 'integrate_proxy' hook.
3604
 *
3605
 * @param string $url The original URL of the requested resource
3606
 * @return string The URL to use
3607
 */
3608
function get_proxied_url($url)
3609
{
3610
	global $boardurl, $image_proxy_enabled, $image_proxy_secret, $user_info;
3611
3612
	// Only use the proxy if enabled, and never for robots
3613
	if (empty($image_proxy_enabled) || !empty($user_info['possibly_robot']))
3614
		return $url;
3615
3616
	$parsedurl = parse_url($url);
3617
3618
	// Don't bother with HTTPS URLs, schemeless URLs, or obviously invalid URLs
3619
	if (empty($parsedurl['scheme']) || empty($parsedurl['host']) || empty($parsedurl['path']) || $parsedurl['scheme'] === 'https')
3620
		return $url;
3621
3622
	// We don't need to proxy our own resources
3623
	if ($parsedurl['host'] === parse_url($boardurl, PHP_URL_HOST))
3624
		return strtr($url, array('http://' => 'https://'));
3625
3626
	// By default, use SMF's own image proxy script
3627
	$proxied_url = strtr($boardurl, array('http://' => 'https://')) . '/proxy.php?request=' . urlencode($url) . '&hash=' . hash_hmac('sha1', $url, $image_proxy_secret);
3628
3629
	// Allow mods to easily implement an alternative proxy
3630
	// MOD AUTHORS: To add settings UI for your proxy, use the integrate_general_settings hook.
3631
	call_integration_hook('integrate_proxy', array($url, &$proxied_url));
3632
3633
	return $proxied_url;
3634
}
3635
3636
/**
3637
 * Make sure the browser doesn't come back and repost the form data.
3638
 * Should be used whenever anything is posted.
3639
 *
3640
 * @param string $setLocation The URL to redirect them to
3641
 * @param bool $refresh Whether to use a meta refresh instead
3642
 * @param bool $permanent Whether to send a 301 Moved Permanently instead of a 302 Moved Temporarily
3643
 */
3644
function redirectexit($setLocation = '', $refresh = false, $permanent = false)
3645
{
3646
	global $scripturl, $context, $modSettings, $db_show_debug, $db_cache;
3647
3648
	// In case we have mail to send, better do that - as obExit doesn't always quite make it...
3649
	if (!empty($context['flush_mail']))
3650
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
3651
		AddMailQueue(true);
3652
3653
	$add = preg_match('~^(ftp|http)[s]?://~', $setLocation) == 0 && substr($setLocation, 0, 6) != 'about:';
3654
3655
	if ($add)
3656
		$setLocation = $scripturl . ($setLocation != '' ? '?' . $setLocation : '');
3657
3658
	// Put the session ID in.
3659
	if (defined('SID') && SID != '')
3660
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', $scripturl . '?' . SID . ';', $setLocation);
3661
	// Keep that debug in their for template debugging!
3662
	elseif (isset($_GET['debug']))
3663
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);
3664
3665
	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'])))
3666
	{
3667
		if (defined('SID') && SID != '')
3668
			$setLocation = preg_replace_callback(
3669
				'~^' . preg_quote($scripturl, '~') . '\?(?:' . SID . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~',
3670
				function($m) use ($scripturl)
3671
				{
3672
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . SID . (isset($m[2]) ? "$m[2]" : "");
3673
				},
3674
				$setLocation
3675
			);
3676
		else
3677
			$setLocation = preg_replace_callback(
3678
				'~^' . preg_quote($scripturl, '~') . '\?((?:board|topic)=[^#"]+?)(#[^"]*?)?$~',
3679
				function($m) use ($scripturl)
3680
				{
3681
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html' . (isset($m[2]) ? "$m[2]" : "");
3682
				},
3683
				$setLocation
3684
			);
3685
	}
3686
3687
	// Maybe integrations want to change where we are heading?
3688
	call_integration_hook('integrate_redirect', array(&$setLocation, &$refresh, &$permanent));
3689
3690
	// Set the header.
3691
	header('location: ' . str_replace(' ', '%20', $setLocation), true, $permanent ? 301 : 302);
3692
3693
	// Debugging.
3694
	if (isset($db_show_debug) && $db_show_debug === true)
3695
		$_SESSION['debug_redirect'] = $db_cache;
3696
3697
	obExit(false);
3698
}
3699
3700
/**
3701
 * Ends execution.  Takes care of template loading and remembering the previous URL.
3702
 *
3703
 * @param bool $header Whether to do the header
3704
 * @param bool $do_footer Whether to do the footer
3705
 * @param bool $from_index Whether we're coming from the board index
3706
 * @param bool $from_fatal_error Whether we're coming from a fatal error
3707
 */
3708
function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false)
3709
{
3710
	global $context, $settings, $modSettings, $txt, $smcFunc, $should_log;
3711
	static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
3712
3713
	// Attempt to prevent a recursive loop.
3714
	++$level;
3715
	if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
3716
		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...
3717
	if ($from_fatal_error)
3718
		$has_fatal_error = true;
3719
3720
	// Clear out the stat cache.
3721
	if (function_exists('trackStats'))
3722
		trackStats();
3723
3724
	// If we have mail to send, send it.
3725
	if (function_exists('AddMailQueue') && !empty($context['flush_mail']))
3726
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
3727
		AddMailQueue(true);
3728
3729
	$do_header = $header === null ? !$header_done : $header;
3730
	if ($do_footer === null)
3731
		$do_footer = $do_header;
3732
3733
	// Has the template/header been done yet?
3734
	if ($do_header)
3735
	{
3736
		// Was the page title set last minute? Also update the HTML safe one.
3737
		if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
3738
			$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](html_entity_decode($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3739
3740
		// Start up the session URL fixer.
3741
		ob_start('ob_sessrewrite');
3742
3743
		if (!empty($settings['output_buffers']) && is_string($settings['output_buffers']))
3744
			$buffers = explode(',', $settings['output_buffers']);
3745
		elseif (!empty($settings['output_buffers']))
3746
			$buffers = $settings['output_buffers'];
3747
		else
3748
			$buffers = array();
3749
3750
		if (isset($modSettings['integrate_buffer']))
3751
			$buffers = array_merge(explode(',', $modSettings['integrate_buffer']), $buffers);
3752
3753
		if (!empty($buffers))
3754
			foreach ($buffers as $function)
3755
			{
3756
				$call = call_helper($function, true);
3757
3758
				// Is it valid?
3759
				if (!empty($call))
3760
					ob_start($call);
0 ignored issues
show
Bug introduced by
It seems like $call can also be of type boolean; however, parameter $callback of ob_start() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

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

3760
					ob_start(/** @scrutinizer ignore-type */ $call);
Loading history...
3761
			}
3762
3763
		// Display the screen in the logical order.
3764
		template_header();
3765
		$header_done = true;
3766
	}
3767
	if ($do_footer)
3768
	{
3769
		loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main');
3770
3771
		// Anything special to put out?
3772
		if (!empty($context['insert_after_template']) && !isset($_REQUEST['xml']))
3773
			echo $context['insert_after_template'];
3774
3775
		// Just so we don't get caught in an endless loop of errors from the footer...
3776
		if (!$footer_done)
3777
		{
3778
			$footer_done = true;
3779
			template_footer();
3780
3781
			// (since this is just debugging... it's okay that it's after </html>.)
3782
			if (!isset($_REQUEST['xml']))
3783
				displayDebug();
3784
		}
3785
	}
3786
3787
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
3788
	if ($should_log)
3789
		$_SESSION['old_url'] = $_SERVER['REQUEST_URL'];
3790
3791
	// For session check verification.... don't switch browsers...
3792
	$_SESSION['USER_AGENT'] = empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT'];
3793
3794
	// Hand off the output to the portal, etc. we're integrated with.
3795
	call_integration_hook('integrate_exit', array($do_footer));
3796
3797
	// Don't exit if we're coming from index.php; that will pass through normally.
3798
	if (!$from_index)
3799
		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...
3800
}
3801
3802
/**
3803
 * Get the size of a specified image with better error handling.
3804
 *
3805
 * @todo see if it's better in Subs-Graphics, but one step at the time.
3806
 * Uses getimagesize() to determine the size of a file.
3807
 * Attempts to connect to the server first so it won't time out.
3808
 *
3809
 * @param string $url The URL of the image
3810
 * @return array|false The image size as array (width, height), or false on failure
3811
 */
3812
function url_image_size($url)
3813
{
3814
	global $sourcedir;
3815
3816
	// Make sure it is a proper URL.
3817
	$url = str_replace(' ', '%20', $url);
3818
3819
	// Can we pull this from the cache... please please?
3820
	if (($temp = cache_get_data('url_image_size-' . md5($url), 240)) !== null)
3821
		return $temp;
3822
	$t = microtime(true);
3823
3824
	// Get the host to pester...
3825
	preg_match('~^\w+://(.+?)/(.*)$~', $url, $match);
3826
3827
	// Can't figure it out, just try the image size.
3828
	if ($url == '' || $url == 'http://' || $url == 'https://')
3829
	{
3830
		return false;
3831
	}
3832
	elseif (!isset($match[1]))
3833
	{
3834
		$size = @getimagesize($url);
3835
	}
3836
	else
3837
	{
3838
		// Try to connect to the server... give it half a second.
3839
		$temp = 0;
3840
		$fp = @fsockopen($match[1], 80, $temp, $temp, 0.5);
3841
3842
		// Successful?  Continue...
3843
		if ($fp != false)
3844
		{
3845
			// Send the HEAD request (since we don't have to worry about chunked, HTTP/1.1 is fine here.)
3846
			fwrite($fp, 'HEAD /' . $match[2] . ' HTTP/1.1' . "\r\n" . 'Host: ' . $match[1] . "\r\n" . 'user-agent: '. SMF_USER_AGENT . "\r\n" . 'Connection: close' . "\r\n\r\n");
3847
3848
			// Read in the HTTP/1.1 or whatever.
3849
			$test = substr(fgets($fp, 11), -1);
3850
			fclose($fp);
3851
3852
			// See if it returned a 404/403 or something.
3853
			if ($test < 4)
3854
			{
3855
				$size = @getimagesize($url);
3856
3857
				// This probably means allow_url_fopen is off, let's try GD.
3858
				if ($size === false && function_exists('imagecreatefromstring'))
3859
				{
3860
					// It's going to hate us for doing this, but another request...
3861
					$image = @imagecreatefromstring(fetch_web_data($url));
0 ignored issues
show
Bug introduced by
It seems like fetch_web_data($url) can also be of type false; however, parameter $image of imagecreatefromstring() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

3861
					$image = @imagecreatefromstring(/** @scrutinizer ignore-type */ fetch_web_data($url));
Loading history...
3862
					if ($image !== false)
3863
					{
3864
						$size = array(imagesx($image), imagesy($image));
3865
						imagedestroy($image);
3866
					}
3867
				}
3868
			}
3869
		}
3870
	}
3871
3872
	// If we didn't get it, we failed.
3873
	if (!isset($size))
3874
		$size = false;
3875
3876
	// If this took a long time, we may never have to do it again, but then again we might...
3877
	if (microtime(true) - $t > 0.8)
3878
		cache_put_data('url_image_size-' . md5($url), $size, 240);
3879
3880
	// Didn't work.
3881
	return $size;
3882
}
3883
3884
/**
3885
 * Sets up the basic theme context stuff.
3886
 *
3887
 * @param bool $forceload Whether to load the theme even if it's already loaded
3888
 */
3889
function setupThemeContext($forceload = false)
3890
{
3891
	global $modSettings, $user_info, $scripturl, $context, $settings, $options, $txt, $maintenance;
3892
	global $smcFunc;
3893
	static $loaded = false;
3894
3895
	// Under SSI this function can be called more then once.  That can cause some problems.
3896
	//   So only run the function once unless we are forced to run it again.
3897
	if ($loaded && !$forceload)
3898
		return;
3899
3900
	$loaded = true;
3901
3902
	$context['in_maintenance'] = !empty($maintenance);
3903
	$context['current_time'] = timeformat(time(), false);
3904
	$context['current_action'] = isset($_GET['action']) ? $smcFunc['htmlspecialchars']($_GET['action']) : '';
3905
	$context['random_news_line'] = array();
3906
3907
	// Get some news...
3908
	$context['news_lines'] = array_filter(explode("\n", str_replace("\r", '', trim(addslashes($modSettings['news'])))));
3909
	for ($i = 0, $n = count($context['news_lines']); $i < $n; $i++)
3910
	{
3911
		if (trim($context['news_lines'][$i]) == '')
3912
			continue;
3913
3914
		// Clean it up for presentation ;).
3915
		$context['news_lines'][$i] = parse_bbc(stripslashes(trim($context['news_lines'][$i])), true, 'news' . $i);
3916
	}
3917
3918
	if (!empty($context['news_lines']) && (!empty($modSettings['allow_guestAccess']) || $context['user']['is_logged']))
3919
		$context['random_news_line'] = $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)];
3920
3921
	if (!$user_info['is_guest'])
3922
	{
3923
		$context['user']['messages'] = &$user_info['messages'];
3924
		$context['user']['unread_messages'] = &$user_info['unread_messages'];
3925
		$context['user']['alerts'] = &$user_info['alerts'];
3926
3927
		// Personal message popup...
3928
		if ($user_info['unread_messages'] > (isset($_SESSION['unread_messages']) ? $_SESSION['unread_messages'] : 0))
3929
			$context['user']['popup_messages'] = true;
3930
		else
3931
			$context['user']['popup_messages'] = false;
3932
		$_SESSION['unread_messages'] = $user_info['unread_messages'];
3933
3934
		if (allowedTo('moderate_forum'))
3935
			$context['unapproved_members'] = !empty($modSettings['unapprovedMembers']) ? $modSettings['unapprovedMembers'] : 0;
3936
3937
		$context['user']['avatar'] = array();
3938
3939
		// Check for gravatar first since we might be forcing them...
3940
		if (!empty($modSettings['gravatarEnabled']) && (substr($user_info['avatar']['url'], 0, 11) == 'gravatar://' || !empty($modSettings['gravatarOverride'])))
3941
		{
3942
			if (!empty($modSettings['gravatarAllowExtraEmail']) && stristr($user_info['avatar']['url'], 'gravatar://') && strlen($user_info['avatar']['url']) > 11)
3943
				$context['user']['avatar']['href'] = get_gravatar_url($smcFunc['substr']($user_info['avatar']['url'], 11));
3944
			else
3945
				$context['user']['avatar']['href'] = get_gravatar_url($user_info['email']);
3946
		}
3947
		// Uploaded?
3948
		elseif ($user_info['avatar']['url'] == '' && !empty($user_info['avatar']['id_attach']))
3949
			$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';
3950
		// Full URL?
3951
		elseif (strpos($user_info['avatar']['url'], 'http://') === 0 || strpos($user_info['avatar']['url'], 'https://') === 0)
3952
			$context['user']['avatar']['href'] = $user_info['avatar']['url'];
3953
		// Otherwise we assume it's server stored.
3954
		elseif ($user_info['avatar']['url'] != '')
3955
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/' . $smcFunc['htmlspecialchars']($user_info['avatar']['url']);
3956
		// No avatar at all? Fine, we have a big fat default avatar ;)
3957
		else
3958
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/default.png';
3959
3960
		if (!empty($context['user']['avatar']))
3961
			$context['user']['avatar']['image'] = '<img src="' . $context['user']['avatar']['href'] . '" alt="" class="avatar">';
3962
3963
		// Figure out how long they've been logged in.
3964
		$context['user']['total_time_logged_in'] = array(
3965
			'days' => floor($user_info['total_time_logged_in'] / 86400),
3966
			'hours' => floor(($user_info['total_time_logged_in'] % 86400) / 3600),
3967
			'minutes' => floor(($user_info['total_time_logged_in'] % 3600) / 60)
3968
		);
3969
	}
3970
	else
3971
	{
3972
		$context['user']['messages'] = 0;
3973
		$context['user']['unread_messages'] = 0;
3974
		$context['user']['avatar'] = array();
3975
		$context['user']['total_time_logged_in'] = array('days' => 0, 'hours' => 0, 'minutes' => 0);
3976
		$context['user']['popup_messages'] = false;
3977
3978
		// If we've upgraded recently, go easy on the passwords.
3979
		if (!empty($modSettings['disableHashTime']) && ($modSettings['disableHashTime'] == 1 || time() < $modSettings['disableHashTime']))
3980
			$context['disable_login_hashing'] = true;
3981
	}
3982
3983
	// Setup the main menu items.
3984
	setupMenuContext();
3985
3986
	// This is here because old index templates might still use it.
3987
	$context['show_news'] = !empty($settings['enable_news']);
3988
3989
	// This is done to allow theme authors to customize it as they want.
3990
	$context['show_pm_popup'] = $context['user']['popup_messages'] && !empty($options['popup_messages']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != 'pm');
3991
3992
	// 2.1+: Add the PM popup here instead. Theme authors can still override it simply by editing/removing the 'fPmPopup' in the array.
3993
	if ($context['show_pm_popup'])
3994
		addInlineJavaScript('
3995
		jQuery(document).ready(function($) {
3996
			new smc_Popup({
3997
				heading: ' . JavaScriptEscape($txt['show_personal_messages_heading']) . ',
3998
				content: ' . JavaScriptEscape(sprintf($txt['show_personal_messages'], $context['user']['unread_messages'], $scripturl . '?action=pm')) . ',
3999
				icon_class: \'main_icons mail_new\'
4000
			});
4001
		});');
4002
4003
	// Add a generic "Are you sure?" confirmation message.
4004
	addInlineJavaScript('
4005
	var smf_you_sure =' . JavaScriptEscape($txt['quickmod_confirm']) . ';');
4006
4007
	// Now add the capping code for avatars.
4008
	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')
4009
		addInlineCss('
4010
	img.avatar { max-width: ' . $modSettings['avatar_max_width_external'] . 'px !important; max-height: ' . $modSettings['avatar_max_height_external'] . 'px !important; }');
4011
4012
	// Add max image limits
4013
	if (!empty($modSettings['max_image_width']))
4014
		addInlineCss('
4015
	.postarea .bbc_img, .list_posts .bbc_img, .post .inner .bbc_img, form#reported_posts .bbc_img, #preview_body .bbc_img { max-width: min(100%,' . $modSettings['max_image_width'] . 'px); }');
4016
4017
	if (!empty($modSettings['max_image_height']))
4018
		addInlineCss('
4019
	.postarea .bbc_img, .list_posts .bbc_img, .post .inner .bbc_img, form#reported_posts .bbc_img, #preview_body .bbc_img { max-height: ' . $modSettings['max_image_height'] . 'px; }');
4020
4021
	// This looks weird, but it's because BoardIndex.php references the variable.
4022
	$context['common_stats']['latest_member'] = array(
4023
		'id' => $modSettings['latestMember'],
4024
		'name' => $modSettings['latestRealName'],
4025
		'href' => $scripturl . '?action=profile;u=' . $modSettings['latestMember'],
4026
		'link' => '<a href="' . $scripturl . '?action=profile;u=' . $modSettings['latestMember'] . '">' . $modSettings['latestRealName'] . '</a>',
4027
	);
4028
	$context['common_stats'] = array(
4029
		'total_posts' => comma_format($modSettings['totalMessages']),
4030
		'total_topics' => comma_format($modSettings['totalTopics']),
4031
		'total_members' => comma_format($modSettings['totalMembers']),
4032
		'latest_member' => $context['common_stats']['latest_member'],
4033
	);
4034
	$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']);
4035
4036
	if (empty($settings['theme_version']))
4037
		addJavaScriptVar('smf_scripturl', $scripturl);
4038
4039
	if (!isset($context['page_title']))
4040
		$context['page_title'] = '';
4041
4042
	// Set some specific vars.
4043
	$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](html_entity_decode($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
4044
	$context['meta_keywords'] = !empty($modSettings['meta_keywords']) ? $smcFunc['htmlspecialchars']($modSettings['meta_keywords']) : '';
4045
4046
	// Content related meta tags, including Open Graph
4047
	$context['meta_tags'][] = array('property' => 'og:site_name', 'content' => $context['forum_name']);
4048
	$context['meta_tags'][] = array('property' => 'og:title', 'content' => $context['page_title_html_safe']);
4049
4050
	if (!empty($context['meta_keywords']))
4051
		$context['meta_tags'][] = array('name' => 'keywords', 'content' => $context['meta_keywords']);
4052
4053
	if (!empty($context['canonical_url']))
4054
		$context['meta_tags'][] = array('property' => 'og:url', 'content' => $context['canonical_url']);
4055
4056
	if (!empty($settings['og_image']))
4057
		$context['meta_tags'][] = array('property' => 'og:image', 'content' => $settings['og_image']);
4058
4059
	if (!empty($context['meta_description']))
4060
	{
4061
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['meta_description']);
4062
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['meta_description']);
4063
	}
4064
	else
4065
	{
4066
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['page_title_html_safe']);
4067
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['page_title_html_safe']);
4068
	}
4069
4070
	call_integration_hook('integrate_theme_context');
4071
}
4072
4073
/**
4074
 * Helper function to set the system memory to a needed value
4075
 * - If the needed memory is greater than current, will attempt to get more
4076
 * - if in_use is set to true, will also try to take the current memory usage in to account
4077
 *
4078
 * @param string $needed The amount of memory to request, if needed, like 256M
4079
 * @param bool $in_use Set to true to account for current memory usage of the script
4080
 * @return boolean True if we have at least the needed memory
4081
 */
4082
function setMemoryLimit($needed, $in_use = false)
4083
{
4084
	// everything in bytes
4085
	$memory_current = memoryReturnBytes(ini_get('memory_limit'));
4086
	$memory_needed = memoryReturnBytes($needed);
4087
4088
	// should we account for how much is currently being used?
4089
	if ($in_use)
4090
		$memory_needed += function_exists('memory_get_usage') ? memory_get_usage() : (2 * 1048576);
4091
4092
	// if more is needed, request it
4093
	if ($memory_current < $memory_needed)
4094
	{
4095
		@ini_set('memory_limit', ceil($memory_needed / 1048576) . 'M');
4096
		$memory_current = memoryReturnBytes(ini_get('memory_limit'));
4097
	}
4098
4099
	$memory_current = max($memory_current, memoryReturnBytes(get_cfg_var('memory_limit')));
0 ignored issues
show
Bug introduced by
It seems like get_cfg_var('memory_limit') can also be of type array; however, parameter $val of memoryReturnBytes() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

4099
	$memory_current = max($memory_current, memoryReturnBytes(/** @scrutinizer ignore-type */ get_cfg_var('memory_limit')));
Loading history...
4100
4101
	// return success or not
4102
	return (bool) ($memory_current >= $memory_needed);
4103
}
4104
4105
/**
4106
 * Helper function to convert memory string settings to bytes
4107
 *
4108
 * @param string $val The byte string, like 256M or 1G
4109
 * @return integer The string converted to a proper integer in bytes
4110
 */
4111
function memoryReturnBytes($val)
4112
{
4113
	if (is_integer($val))
0 ignored issues
show
introduced by
The condition is_integer($val) is always false.
Loading history...
4114
		return $val;
4115
4116
	// Separate the number from the designator
4117
	$val = trim($val);
4118
	$num = intval(substr($val, 0, strlen($val) - 1));
4119
	$last = strtolower(substr($val, -1));
4120
4121
	// convert to bytes
4122
	switch ($last)
4123
	{
4124
		case 'g':
4125
			$num *= 1024;
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
4126
		case 'm':
4127
			$num *= 1024;
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
4128
		case 'k':
4129
			$num *= 1024;
4130
	}
4131
	return $num;
4132
}
4133
4134
/**
4135
 * The header template
4136
 */
4137
function template_header()
4138
{
4139
	global $txt, $modSettings, $context, $user_info, $boarddir, $cachedir, $cache_enable, $language;
4140
4141
	setupThemeContext();
4142
4143
	// Print stuff to prevent caching of pages (except on attachment errors, etc.)
4144
	if (empty($context['no_last_modified']))
4145
	{
4146
		header('expires: Mon, 26 Jul 1997 05:00:00 GMT');
4147
		header('last-modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
4148
4149
		// Are we debugging the template/html content?
4150
		if (!isset($_REQUEST['xml']) && isset($_GET['debug']) && !isBrowser('ie'))
4151
			header('content-type: application/xhtml+xml');
4152
		elseif (!isset($_REQUEST['xml']))
4153
			header('content-type: text/html; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
4154
	}
4155
4156
	header('content-type: text/' . (isset($_REQUEST['xml']) ? 'xml' : 'html') . '; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
4157
4158
	// We need to splice this in after the body layer, or after the main layer for older stuff.
4159
	if ($context['in_maintenance'] && $context['user']['is_admin'])
4160
	{
4161
		$position = array_search('body', $context['template_layers']);
4162
		if ($position === false)
4163
			$position = array_search('main', $context['template_layers']);
4164
4165
		if ($position !== false)
4166
		{
4167
			$before = array_slice($context['template_layers'], 0, $position + 1);
4168
			$after = array_slice($context['template_layers'], $position + 1);
4169
			$context['template_layers'] = array_merge($before, array('maint_warning'), $after);
4170
		}
4171
	}
4172
4173
	$checked_securityFiles = false;
4174
	$showed_banned = false;
4175
	foreach ($context['template_layers'] as $layer)
4176
	{
4177
		loadSubTemplate($layer . '_above', true);
4178
4179
		// May seem contrived, but this is done in case the body and main layer aren't there...
4180
		if (in_array($layer, array('body', 'main')) && allowedTo('admin_forum') && !$user_info['is_guest'] && !$checked_securityFiles)
4181
		{
4182
			$checked_securityFiles = true;
4183
4184
			$securityFiles = array('install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~');
4185
4186
			// Add your own files.
4187
			call_integration_hook('integrate_security_files', array(&$securityFiles));
4188
4189
			foreach ($securityFiles as $i => $securityFile)
4190
			{
4191
				if (!file_exists($boarddir . '/' . $securityFile))
4192
					unset($securityFiles[$i]);
4193
			}
4194
4195
			// We are already checking so many files...just few more doesn't make any difference! :P
4196
			if (!empty($modSettings['currentAttachmentUploadDir']))
4197
				$path = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
4198
4199
			else
4200
				$path = $modSettings['attachmentUploadDir'];
4201
4202
			secureDirectory($path, true);
4203
			secureDirectory($cachedir);
4204
4205
			// If agreement is enabled, at least the english version shall exist
4206
			if (!empty($modSettings['requireAgreement']))
4207
				$agreement = !file_exists($boarddir . '/agreement.txt');
4208
4209
			// If privacy policy is enabled, at least the default language version shall exist
4210
			if (!empty($modSettings['requirePolicyAgreement']))
4211
				$policy_agreement = empty($modSettings['policy_' . $language]);
4212
4213
			if (!empty($securityFiles) ||
4214
				(!empty($cache_enable) && !is_writable($cachedir)) ||
4215
				!empty($agreement) ||
4216
				!empty($policy_agreement) ||
4217
				!empty($context['auth_secret_missing']))
4218
			{
4219
				echo '
4220
		<div class="errorbox">
4221
			<p class="alert">!!</p>
4222
			<h3>', empty($securityFiles) && empty($context['auth_secret_missing']) ? $txt['generic_warning'] : $txt['security_risk'], '</h3>
4223
			<p>';
4224
4225
				foreach ($securityFiles as $securityFile)
4226
				{
4227
					echo '
4228
				', $txt['not_removed'], '<strong>', $securityFile, '</strong>!<br>';
4229
4230
					if ($securityFile == 'Settings.php~' || $securityFile == 'Settings_bak.php~')
4231
						echo '
4232
				', sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)), '<br>';
4233
				}
4234
4235
				if (!empty($cache_enable) && !is_writable($cachedir))
4236
					echo '
4237
				<strong>', $txt['cache_writable'], '</strong><br>';
4238
4239
				if (!empty($agreement))
4240
					echo '
4241
				<strong>', $txt['agreement_missing'], '</strong><br>';
4242
4243
				if (!empty($policy_agreement))
4244
					echo '
4245
				<strong>', $txt['policy_agreement_missing'], '</strong><br>';
4246
4247
				if (!empty($context['auth_secret_missing']))
4248
					echo '
4249
				<strong>', $txt['auth_secret_missing'], '</strong><br>';
4250
4251
				echo '
4252
			</p>
4253
		</div>';
4254
			}
4255
		}
4256
		// If the user is banned from posting inform them of it.
4257
		elseif (in_array($layer, array('main', 'body')) && isset($_SESSION['ban']['cannot_post']) && !$showed_banned)
4258
		{
4259
			$showed_banned = true;
4260
			echo '
4261
				<div class="windowbg alert" style="margin: 2ex; padding: 2ex; border: 2px dashed red;">
4262
					', sprintf($txt['you_are_post_banned'], $user_info['is_guest'] ? $txt['guest_title'] : $user_info['name']);
4263
4264
			if (!empty($_SESSION['ban']['cannot_post']['reason']))
4265
				echo '
4266
					<div style="padding-left: 4ex; padding-top: 1ex;">', $_SESSION['ban']['cannot_post']['reason'], '</div>';
4267
4268
			if (!empty($_SESSION['ban']['expire_time']))
4269
				echo '
4270
					<div>', sprintf($txt['your_ban_expires'], timeformat($_SESSION['ban']['expire_time'], false)), '</div>';
4271
			else
4272
				echo '
4273
					<div>', $txt['your_ban_expires_never'], '</div>';
4274
4275
			echo '
4276
				</div>';
4277
		}
4278
	}
4279
}
4280
4281
/**
4282
 * Show the copyright.
4283
 */
4284
function theme_copyright()
4285
{
4286
	global $forum_copyright, $scripturl;
4287
4288
	// Don't display copyright for things like SSI.
4289
	if (SMF !== 1)
0 ignored issues
show
introduced by
The condition SMF !== 1 is always true.
Loading history...
4290
		return;
4291
4292
	// Put in the version...
4293
	printf($forum_copyright, SMF_FULL_VERSION, SMF_SOFTWARE_YEAR, $scripturl);
4294
}
4295
4296
/**
4297
 * The template footer
4298
 */
4299
function template_footer()
4300
{
4301
	global $context, $modSettings, $db_count;
4302
4303
	// Show the load time?  (only makes sense for the footer.)
4304
	$context['show_load_time'] = !empty($modSettings['timeLoadPageEnable']);
4305
	$context['load_time'] = round(microtime(true) - TIME_START, 3);
4306
	$context['load_queries'] = $db_count;
4307
4308
	if (!empty($context['template_layers']) && is_array($context['template_layers']))
4309
		foreach (array_reverse($context['template_layers']) as $layer)
4310
			loadSubTemplate($layer . '_below', true);
4311
}
4312
4313
/**
4314
 * Output the Javascript files
4315
 * 	- tabbing in this function is to make the HTML source look good and proper
4316
 *  - if deferred is set function will output all JS set to load at page end
4317
 *
4318
 * @param bool $do_deferred If true will only output the deferred JS (the stuff that goes right before the closing body tag)
4319
 */
4320
function template_javascript($do_deferred = false)
4321
{
4322
	global $context, $modSettings, $settings;
4323
4324
	// Use this hook to minify/optimize Javascript files and vars
4325
	call_integration_hook('integrate_pre_javascript_output', array(&$do_deferred));
4326
4327
	$toMinify = array(
4328
		'standard' => array(),
4329
		'defer' => array(),
4330
		'async' => array(),
4331
	);
4332
4333
	// Ouput the declared Javascript variables.
4334
	if (!empty($context['javascript_vars']) && !$do_deferred)
4335
	{
4336
		echo '
4337
	<script>';
4338
4339
		foreach ($context['javascript_vars'] as $key => $value)
4340
		{
4341
			if (!is_string($key) || is_numeric($key))
4342
				continue;
4343
4344
			if (!is_string($value) && !is_numeric($value))
4345
				$value = null;
4346
4347
			echo "\n\t\t", 'var ', $key, isset($value) ? ' = ' . $value : '', ';';
4348
		}
4349
4350
		echo '
4351
	</script>';
4352
	}
4353
4354
	// In the dark days before HTML5, deferred JS files needed to be loaded at the end of the body.
4355
	// Now we load them in the head and use 'async' and/or 'defer' attributes. Much better performance.
4356
	if (!$do_deferred)
4357
	{
4358
		// While we have JavaScript files to place in the template.
4359
		foreach ($context['javascript_files'] as $id => $js_file)
4360
		{
4361
			// Last minute call! allow theme authors to disable single files.
4362
			if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
4363
				continue;
4364
4365
			// By default files don't get minimized unless the file explicitly says so!
4366
			if (!empty($js_file['options']['minimize']) && !empty($modSettings['minimize_files']))
4367
			{
4368
				if (!empty($js_file['options']['async']))
4369
					$toMinify['async'][] = $js_file;
4370
4371
				elseif (!empty($js_file['options']['defer']))
4372
					$toMinify['defer'][] = $js_file;
4373
4374
				else
4375
					$toMinify['standard'][] = $js_file;
4376
4377
				// Grab a random seed.
4378
				if (!isset($minSeed) && isset($js_file['options']['seed']))
4379
					$minSeed = $js_file['options']['seed'];
4380
			}
4381
4382
			else
4383
			{
4384
				echo '
4385
	<script src="', $js_file['fileUrl'], isset($js_file['options']['seed']) ? $js_file['options']['seed'] : '', '"', !empty($js_file['options']['async']) ? ' async' : '', !empty($js_file['options']['defer']) ? ' defer' : '';
4386
4387
				if (!empty($js_file['options']['attributes']))
4388
					foreach ($js_file['options']['attributes'] as $key => $value)
4389
					{
4390
						if (is_bool($value))
4391
							echo !empty($value) ? ' ' . $key : '';
4392
4393
						else
4394
							echo ' ', $key, '="', $value, '"';
4395
					}
4396
4397
				echo '></script>';
4398
			}
4399
		}
4400
4401
		foreach ($toMinify as $js_files)
4402
		{
4403
			if (!empty($js_files))
4404
			{
4405
				$result = custMinify($js_files, 'js');
4406
4407
				$minSuccessful = array_keys($result) === array('smf_minified');
4408
4409
				foreach ($result as $minFile)
4410
					echo '
4411
	<script src="', $minFile['fileUrl'], $minSuccessful && isset($minSeed) ? $minSeed : '', '"', !empty($minFile['options']['async']) ? ' async' : '', !empty($minFile['options']['defer']) ? ' defer' : '', '></script>';
4412
			}
4413
		}
4414
	}
4415
4416
	// Inline JavaScript - Actually useful some times!
4417
	if (!empty($context['javascript_inline']))
4418
	{
4419
		if (!empty($context['javascript_inline']['defer']) && $do_deferred)
4420
		{
4421
			echo '
4422
<script>
4423
window.addEventListener("DOMContentLoaded", function() {';
4424
4425
			foreach ($context['javascript_inline']['defer'] as $js_code)
4426
				echo $js_code;
4427
4428
			echo '
4429
});
4430
</script>';
4431
		}
4432
4433
		if (!empty($context['javascript_inline']['standard']) && !$do_deferred)
4434
		{
4435
			echo '
4436
	<script>';
4437
4438
			foreach ($context['javascript_inline']['standard'] as $js_code)
4439
				echo $js_code;
4440
4441
			echo '
4442
	</script>';
4443
		}
4444
	}
4445
}
4446
4447
/**
4448
 * Output the CSS files
4449
 */
4450
function template_css()
4451
{
4452
	global $context, $db_show_debug, $boardurl, $settings, $modSettings;
4453
4454
	// Use this hook to minify/optimize CSS files
4455
	call_integration_hook('integrate_pre_css_output');
4456
4457
	$toMinify = array();
4458
	$normal = array();
4459
4460
	uasort(
4461
		$context['css_files'],
4462
		function ($a, $b)
4463
		{
4464
			return $a['options']['order_pos'] < $b['options']['order_pos'] ? -1 : ($a['options']['order_pos'] > $b['options']['order_pos'] ? 1 : 0);
4465
		}
4466
	);
4467
4468
	foreach ($context['css_files'] as $id => $file)
4469
	{
4470
		// Last minute call! allow theme authors to disable single files.
4471
		if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
4472
			continue;
4473
4474
		// Files are minimized unless they explicitly opt out.
4475
		if (!isset($file['options']['minimize']))
4476
			$file['options']['minimize'] = true;
4477
4478
		if (!empty($file['options']['minimize']) && !empty($modSettings['minimize_files']) && !isset($_REQUEST['normalcss']))
4479
		{
4480
			$toMinify[] = $file;
4481
4482
			// Grab a random seed.
4483
			if (!isset($minSeed) && isset($file['options']['seed']))
4484
				$minSeed = $file['options']['seed'];
4485
		}
4486
		else
4487
			$normal[] = array(
4488
				'url' => $file['fileUrl'] . (isset($file['options']['seed']) ? $file['options']['seed'] : ''),
4489
				'attributes' => !empty($file['options']['attributes']) ? $file['options']['attributes'] : array()
4490
			);
4491
	}
4492
4493
	if (!empty($toMinify))
4494
	{
4495
		$result = custMinify($toMinify, 'css');
4496
4497
		$minSuccessful = array_keys($result) === array('smf_minified');
4498
4499
		foreach ($result as $minFile)
4500
			echo '
4501
	<link rel="stylesheet" href="', $minFile['fileUrl'], $minSuccessful && isset($minSeed) ? $minSeed : '', '">';
4502
	}
4503
4504
	// Print the rest after the minified files.
4505
	if (!empty($normal))
4506
		foreach ($normal as $nf)
4507
		{
4508
			echo '
4509
	<link rel="stylesheet" href="', $nf['url'], '"';
4510
4511
			if (!empty($nf['attributes']))
4512
				foreach ($nf['attributes'] as $key => $value)
4513
				{
4514
					if (is_bool($value))
4515
						echo !empty($value) ? ' ' . $key : '';
4516
					else
4517
						echo ' ', $key, '="', $value, '"';
4518
				}
4519
4520
			echo '>';
4521
		}
4522
4523
	if ($db_show_debug === true)
4524
	{
4525
		// Try to keep only what's useful.
4526
		$repl = array($boardurl . '/Themes/' => '', $boardurl . '/' => '');
4527
		foreach ($context['css_files'] as $file)
4528
			$context['debug']['sheets'][] = strtr($file['fileName'], $repl);
4529
	}
4530
4531
	if (!empty($context['css_header']))
4532
	{
4533
		echo '
4534
	<style>';
4535
4536
		foreach ($context['css_header'] as $css)
4537
			echo $css . '
4538
	';
4539
4540
		echo '
4541
	</style>';
4542
	}
4543
}
4544
4545
/**
4546
 * Get an array of previously defined files and adds them to our main minified files.
4547
 * Sets a one day cache to avoid re-creating a file on every request.
4548
 *
4549
 * @param array $data The files to minify.
4550
 * @param string $type either css or js.
4551
 * @return array Info about the minified file, or about the original files if the minify process failed.
4552
 */
4553
function custMinify($data, $type)
4554
{
4555
	global $settings, $txt;
4556
4557
	$types = array('css', 'js');
4558
	$type = !empty($type) && in_array($type, $types) ? $type : false;
4559
	$data = is_array($data) ? $data : array();
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
4560
4561
	if (empty($type) || empty($data))
4562
		return $data;
4563
4564
	// Different pages include different files, so we use a hash to label the different combinations
4565
	$hash = md5(implode(' ', array_map(
4566
		function($file)
4567
		{
4568
			return $file['filePath'] . '-' . $file['mtime'];
4569
		},
4570
		$data
4571
	)));
4572
4573
	// Is this a deferred or asynchronous JavaScript file?
4574
	$async = $type === 'js';
4575
	$defer = $type === 'js';
4576
	if ($type === 'js')
4577
	{
4578
		foreach ($data as $id => $file)
4579
		{
4580
			// A minified script should only be loaded asynchronously if all its components wanted to be.
4581
			if (empty($file['options']['async']))
4582
				$async = false;
4583
4584
			// A minified script should only be deferred if all its components wanted to be.
4585
			if (empty($file['options']['defer']))
4586
				$defer = false;
4587
		}
4588
	}
4589
4590
	// Did we already do this?
4591
	$minified_file = $settings['theme_dir'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/minified_' . $hash . '.' . $type;
4592
	$already_exists = file_exists($minified_file);
4593
4594
	// Already done?
4595
	if ($already_exists)
4596
	{
4597
		return array('smf_minified' => array(
4598
			'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file),
4599
			'filePath' => $minified_file,
4600
			'fileName' => basename($minified_file),
4601
			'options' => array('async' => !empty($async), 'defer' => !empty($defer)),
4602
		));
4603
	}
4604
	// File has to exist. If it doesn't, try to create it.
4605
	elseif (@fopen($minified_file, 'w') === false || !smf_chmod($minified_file))
4606
	{
4607
		loadLanguage('Errors');
4608
		log_error(sprintf($txt['file_not_created'], $minified_file), 'general');
4609
4610
		// The process failed, so roll back to print each individual file.
4611
		return $data;
4612
	}
4613
4614
	// No namespaces, sorry!
4615
	$classType = 'MatthiasMullie\\Minify\\' . strtoupper($type);
4616
4617
	$minifier = new $classType();
4618
4619
	foreach ($data as $id => $file)
4620
	{
4621
		$toAdd = !empty($file['filePath']) && file_exists($file['filePath']) ? $file['filePath'] : false;
4622
4623
		// The file couldn't be located so it won't be added. Log this error.
4624
		if (empty($toAdd))
4625
		{
4626
			loadLanguage('Errors');
4627
			log_error(sprintf($txt['file_minimize_fail'], !empty($file['fileName']) ? $file['fileName'] : $id), 'general');
4628
			continue;
4629
		}
4630
4631
		// Add this file to the list.
4632
		$minifier->add($toAdd);
4633
	}
4634
4635
	// Create the file.
4636
	$minifier->minify($minified_file);
4637
	unset($minifier);
4638
	clearstatcache();
4639
4640
	// Minify process failed.
4641
	if (!filesize($minified_file))
4642
	{
4643
		loadLanguage('Errors');
4644
		log_error(sprintf($txt['file_not_created'], $minified_file), 'general');
4645
4646
		// The process failed so roll back to print each individual file.
4647
		return $data;
4648
	}
4649
4650
	return array('smf_minified' => array(
4651
		'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file),
4652
		'filePath' => $minified_file,
4653
		'fileName' => basename($minified_file),
4654
		'options' => array('async' => $async, 'defer' => $defer),
4655
	));
4656
}
4657
4658
/**
4659
 * Clears out old minimized CSS and JavaScript files and ensures $modSettings['browser_cache'] is up to date
4660
 */
4661
function deleteAllMinified()
4662
{
4663
	global $smcFunc, $txt, $modSettings;
4664
4665
	$not_deleted = array();
4666
	$most_recent = 0;
4667
4668
	// Kinda sucks that we need to do another query to get all the theme dirs, but c'est la vie.
4669
	$request = $smcFunc['db_query']('', '
4670
		SELECT id_theme AS id, value AS dir
4671
		FROM {db_prefix}themes
4672
		WHERE variable = {string:var}',
4673
		array(
4674
			'var' => 'theme_dir',
4675
		)
4676
	);
4677
	while ($theme = $smcFunc['db_fetch_assoc']($request))
4678
	{
4679
		foreach (array('css', 'js') as $type)
4680
		{
4681
			foreach (glob(rtrim($theme['dir'], '/') . '/' . ($type == 'css' ? 'css' : 'scripts') . '/*.' . $type) as $filename)
4682
			{
4683
				// We want to find the most recent mtime of non-minified files
4684
				if (strpos(pathinfo($filename, PATHINFO_BASENAME), 'minified') === false)
0 ignored issues
show
Bug introduced by
It seems like pathinfo($filename, PATHINFO_BASENAME) can also be of type array; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

4684
				if (strpos(/** @scrutinizer ignore-type */ pathinfo($filename, PATHINFO_BASENAME), 'minified') === false)
Loading history...
4685
					$most_recent = max($modSettings['browser_cache'], (int) @filemtime($filename));
4686
4687
				// Try to delete minified files. Add them to our error list if that fails.
4688
				elseif (!@unlink($filename))
4689
					$not_deleted[] = $filename;
4690
			}
4691
		}
4692
	}
4693
	$smcFunc['db_free_result']($request);
4694
4695
	// This setting tracks the most recent modification time of any of our CSS and JS files
4696
	if ($most_recent > $modSettings['browser_cache'])
4697
		updateSettings(array('browser_cache' => $most_recent));
4698
4699
	// If any of the files could not be deleted, log an error about it.
4700
	if (!empty($not_deleted))
4701
	{
4702
		loadLanguage('Errors');
4703
		log_error(sprintf($txt['unlink_minimized_fail'], implode('<br>', $not_deleted)), 'general');
4704
	}
4705
}
4706
4707
/**
4708
 * Get an attachment's encrypted filename. If $new is true, won't check for file existence.
4709
 *
4710
 * @todo this currently returns the hash if new, and the full filename otherwise.
4711
 * Something messy like that.
4712
 * @todo and of course everything relies on this behavior and work around it. :P.
4713
 * Converters included.
4714
 *
4715
 * @param string $filename The name of the file
4716
 * @param int $attachment_id The ID of the attachment
4717
 * @param string|null $dir Which directory it should be in (null to use current one)
4718
 * @param bool $new Whether this is a new attachment
4719
 * @param string $file_hash The file hash
4720
 * @return string The path to the file
4721
 */
4722
function getAttachmentFilename($filename, $attachment_id, $dir = null, $new = false, $file_hash = '')
4723
{
4724
	global $modSettings, $smcFunc;
4725
4726
	// Just make up a nice hash...
4727
	if ($new)
4728
		return sha1(md5($filename . time()) . mt_rand());
4729
4730
	// Just make sure that attachment id is only a int
4731
	$attachment_id = (int) $attachment_id;
4732
4733
	// Grab the file hash if it wasn't added.
4734
	// Left this for legacy.
4735
	if ($file_hash === '')
4736
	{
4737
		$request = $smcFunc['db_query']('', '
4738
			SELECT file_hash
4739
			FROM {db_prefix}attachments
4740
			WHERE id_attach = {int:id_attach}',
4741
			array(
4742
				'id_attach' => $attachment_id,
4743
			)
4744
		);
4745
4746
		if ($smcFunc['db_num_rows']($request) === 0)
4747
			return false;
4748
4749
		list ($file_hash) = $smcFunc['db_fetch_row']($request);
4750
		$smcFunc['db_free_result']($request);
4751
	}
4752
4753
	// Still no hash? mmm...
4754
	if (empty($file_hash))
4755
		$file_hash = sha1(md5($filename . time()) . mt_rand());
4756
4757
	// Are we using multiple directories?
4758
	if (is_array($modSettings['attachmentUploadDir']))
4759
		$path = $modSettings['attachmentUploadDir'][$dir];
4760
4761
	else
4762
		$path = $modSettings['attachmentUploadDir'];
4763
4764
	return $path . '/' . $attachment_id . '_' . $file_hash . '.dat';
4765
}
4766
4767
/**
4768
 * Convert a single IP to a ranged IP.
4769
 * internal function used to convert a user-readable format to a format suitable for the database.
4770
 *
4771
 * @param string $fullip The full IP
4772
 * @return array An array of IP parts
4773
 */
4774
function ip2range($fullip)
4775
{
4776
	// Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.)
4777
	if ($fullip == 'unknown')
4778
		$fullip = '255.255.255.255';
4779
4780
	$ip_parts = explode('-', $fullip);
4781
	$ip_array = array();
4782
4783
	// if ip 22.12.31.21
4784
	if (count($ip_parts) == 1 && isValidIP($fullip))
4785
	{
4786
		$ip_array['low'] = $fullip;
4787
		$ip_array['high'] = $fullip;
4788
		return $ip_array;
4789
	} // if ip 22.12.* -> 22.12.* - 22.12.*
4790
	elseif (count($ip_parts) == 1)
4791
	{
4792
		$ip_parts[0] = $fullip;
4793
		$ip_parts[1] = $fullip;
4794
	}
4795
4796
	// if ip 22.12.31.21-12.21.31.21
4797
	if (count($ip_parts) == 2 && isValidIP($ip_parts[0]) && isValidIP($ip_parts[1]))
4798
	{
4799
		$ip_array['low'] = $ip_parts[0];
4800
		$ip_array['high'] = $ip_parts[1];
4801
		return $ip_array;
4802
	}
4803
	elseif (count($ip_parts) == 2) // if ip 22.22.*-22.22.*
4804
	{
4805
		$valid_low = isValidIP($ip_parts[0]);
4806
		$valid_high = isValidIP($ip_parts[1]);
4807
		$count = 0;
4808
		$mode = (preg_match('/:/', $ip_parts[0]) > 0 ? ':' : '.');
4809
		$max = ($mode == ':' ? 'ffff' : '255');
4810
		$min = 0;
4811
		if (!$valid_low)
4812
		{
4813
			$ip_parts[0] = preg_replace('/\*/', '0', $ip_parts[0]);
4814
			$valid_low = isValidIP($ip_parts[0]);
4815
			while (!$valid_low)
4816
			{
4817
				$ip_parts[0] .= $mode . $min;
4818
				$valid_low = isValidIP($ip_parts[0]);
4819
				$count++;
4820
				if ($count > 9) break;
4821
			}
4822
		}
4823
4824
		$count = 0;
4825
		if (!$valid_high)
4826
		{
4827
			$ip_parts[1] = preg_replace('/\*/', $max, $ip_parts[1]);
4828
			$valid_high = isValidIP($ip_parts[1]);
4829
			while (!$valid_high)
4830
			{
4831
				$ip_parts[1] .= $mode . $max;
4832
				$valid_high = isValidIP($ip_parts[1]);
4833
				$count++;
4834
				if ($count > 9) break;
4835
			}
4836
		}
4837
4838
		if ($valid_high && $valid_low)
4839
		{
4840
			$ip_array['low'] = $ip_parts[0];
4841
			$ip_array['high'] = $ip_parts[1];
4842
		}
4843
	}
4844
4845
	return $ip_array;
4846
}
4847
4848
/**
4849
 * Lookup an IP; try shell_exec first because we can do a timeout on it.
4850
 *
4851
 * @param string $ip The IP to get the hostname from
4852
 * @return string The hostname
4853
 */
4854
function host_from_ip($ip)
4855
{
4856
	global $modSettings;
4857
4858
	if (($host = cache_get_data('hostlookup-' . $ip, 600)) !== null)
4859
		return $host;
4860
	$t = microtime(true);
4861
4862
	// Try the Linux host command, perhaps?
4863
	if (!isset($host) && (strpos(strtolower(PHP_OS), 'win') === false || strpos(strtolower(PHP_OS), 'darwin') !== false) && mt_rand(0, 1) == 1)
4864
	{
4865
		if (!isset($modSettings['host_to_dis']))
4866
			$test = @shell_exec('host -W 1 ' . @escapeshellarg($ip));
4867
		else
4868
			$test = @shell_exec('host ' . @escapeshellarg($ip));
4869
4870
		// Did host say it didn't find anything?
4871
		if (strpos($test, 'not found') !== false)
0 ignored issues
show
Bug introduced by
It seems like $test can also be of type false; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

4877
		elseif (preg_match('~\s([^\s]+?)\.\s~', /** @scrutinizer ignore-type */ $test, $match) == 1)
Loading history...
4878
			$host = $match[1];
4879
	}
4880
4881
	// This is nslookup; usually only Windows, but possibly some Unix?
4882
	if (!isset($host) && stripos(PHP_OS, 'win') !== false && strpos(strtolower(PHP_OS), 'darwin') === false && mt_rand(0, 1) == 1)
4883
	{
4884
		$test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
4885
		if (strpos($test, 'Non-existent domain') !== false)
4886
			$host = '';
4887
		elseif (preg_match('~Name:\s+([^\s]+)~', $test, $match) == 1)
4888
			$host = $match[1];
4889
	}
4890
4891
	// This is the last try :/.
4892
	if (!isset($host) || $host === false)
4893
		$host = @gethostbyaddr($ip);
4894
4895
	// It took a long time, so let's cache it!
4896
	if (microtime(true) - $t > 0.5)
4897
		cache_put_data('hostlookup-' . $ip, $host, 600);
4898
4899
	return $host;
4900
}
4901
4902
/**
4903
 * Chops a string into words and prepares them to be inserted into (or searched from) the database.
4904
 *
4905
 * @param string $text The text to split into words
4906
 * @param int $max_chars The maximum number of characters per word
4907
 * @param bool $encrypt Whether to encrypt the results
4908
 * @return array An array of ints or words depending on $encrypt
4909
 */
4910
function text2words($text, $max_chars = 20, $encrypt = false)
4911
{
4912
	global $smcFunc, $context;
4913
4914
	// Upgrader may be working on old DBs...
4915
	if (!isset($context['utf8']))
4916
		$context['utf8'] = false;
4917
4918
	// Step 1: Remove entities/things we don't consider words:
4919
	$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>' => ' ')));
4920
4921
	// Step 2: Entities we left to letters, where applicable, lowercase.
4922
	$words = un_htmlspecialchars($smcFunc['strtolower']($words));
4923
4924
	// Step 3: Ready to split apart and index!
4925
	$words = explode(' ', $words);
4926
4927
	if ($encrypt)
4928
	{
4929
		$possible_chars = array_flip(array_merge(range(46, 57), range(65, 90), range(97, 122)));
4930
		$returned_ints = array();
4931
		foreach ($words as $word)
4932
		{
4933
			if (($word = trim($word, '-_\'')) !== '')
4934
			{
4935
				$encrypted = substr(crypt($word, 'uk'), 2, $max_chars);
4936
				$total = 0;
4937
				for ($i = 0; $i < $max_chars; $i++)
4938
					$total += $possible_chars[ord($encrypted[$i])] * pow(63, $i);
4939
				$returned_ints[] = $max_chars == 4 ? min($total, 16777215) : $total;
4940
			}
4941
		}
4942
		return array_unique($returned_ints);
4943
	}
4944
	else
4945
	{
4946
		// Trim characters before and after and add slashes for database insertion.
4947
		$returned_words = array();
4948
		foreach ($words as $word)
4949
			if (($word = trim($word, '-_\'')) !== '')
4950
				$returned_words[] = $max_chars === null ? $word : substr($word, 0, $max_chars);
4951
4952
		// Filter out all words that occur more than once.
4953
		return array_unique($returned_words);
4954
	}
4955
}
4956
4957
/**
4958
 * Creates an image/text button
4959
 *
4960
 * @deprecated since 2.1
4961
 * @param string $name The name of the button (should be a main_icons class or the name of an image)
4962
 * @param string $alt The alt text
4963
 * @param string $label The $txt string to use as the label
4964
 * @param string $custom Custom text/html to add to the img tag (only when using an actual image)
4965
 * @param boolean $force_use Whether to force use of this when template_create_button is available
4966
 * @return string The HTML to display the button
4967
 */
4968
function create_button($name, $alt, $label = '', $custom = '', $force_use = false)
4969
{
4970
	global $settings, $txt;
4971
4972
	// Does the current loaded theme have this and we are not forcing the usage of this function?
4973
	if (function_exists('template_create_button') && !$force_use)
4974
		return template_create_button($name, $alt, $label = '', $custom = '');
4975
4976
	if (!$settings['use_image_buttons'])
4977
		return $txt[$alt];
4978
	elseif (!empty($settings['use_buttons']))
4979
		return '<span class="main_icons ' . $name . '" alt="' . $txt[$alt] . '"></span>' . ($label != '' ? '&nbsp;<strong>' . $txt[$label] . '</strong>' : '');
4980
	else
4981
		return '<img src="' . $settings['lang_images_url'] . '/' . $name . '" alt="' . $txt[$alt] . '" ' . $custom . '>';
4982
}
4983
4984
/**
4985
 * Sets up all of the top menu buttons
4986
 * Saves them in the cache if it is available and on
4987
 * Places the results in $context
4988
 */
4989
function setupMenuContext()
4990
{
4991
	global $context, $modSettings, $user_info, $txt, $scripturl, $sourcedir, $settings, $smcFunc, $cache_enable;
4992
4993
	// Set up the menu privileges.
4994
	$context['allow_search'] = !empty($modSettings['allow_guestAccess']) ? allowedTo('search_posts') : (!$user_info['is_guest'] && allowedTo('search_posts'));
4995
	$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'));
4996
4997
	$context['allow_memberlist'] = allowedTo('view_mlist');
4998
	$context['allow_calendar'] = allowedTo('calendar_view') && !empty($modSettings['cal_enabled']);
4999
	$context['allow_moderation_center'] = $context['user']['can_mod'];
5000
	$context['allow_pm'] = allowedTo('pm_read');
5001
5002
	$cacheTime = $modSettings['lastActive'] * 60;
5003
5004
	// Initial "can you post an event in the calendar" option - but this might have been set in the calendar already.
5005
	if (!isset($context['allow_calendar_event']))
5006
	{
5007
		$context['allow_calendar_event'] = $context['allow_calendar'] && allowedTo('calendar_post');
5008
5009
		// If you don't allow events not linked to posts and you're not an admin, we have more work to do...
5010
		if ($context['allow_calendar'] && $context['allow_calendar_event'] && empty($modSettings['cal_allow_unlinked']) && !$user_info['is_admin'])
5011
		{
5012
			$boards_can_post = boardsAllowedTo('post_new');
5013
			$context['allow_calendar_event'] &= !empty($boards_can_post);
5014
		}
5015
	}
5016
5017
	// There is some menu stuff we need to do if we're coming at this from a non-guest perspective.
5018
	if (!$context['user']['is_guest'])
5019
	{
5020
		addInlineJavaScript('
5021
	var user_menus = new smc_PopupMenu();
5022
	user_menus.add("profile", "' . $scripturl . '?action=profile;area=popup");
5023
	user_menus.add("alerts", "' . $scripturl . '?action=profile;area=alerts_popup;u=' . $context['user']['id'] . '");', true);
5024
		if ($context['allow_pm'])
5025
			addInlineJavaScript('
5026
	user_menus.add("pm", "' . $scripturl . '?action=pm;sa=popup");', true);
5027
5028
		if (!empty($modSettings['enable_ajax_alerts']))
5029
		{
5030
			require_once($sourcedir . '/Subs-Notify.php');
5031
5032
			$timeout = getNotifyPrefs($context['user']['id'], 'alert_timeout', true);
5033
			$timeout = empty($timeout) ? 10000 : $timeout[$context['user']['id']]['alert_timeout'] * 1000;
5034
5035
			addInlineJavaScript('
5036
	var new_alert_title = "' . $context['forum_name_html_safe'] . '";
5037
	var alert_timeout = ' . $timeout . ';');
5038
			loadJavaScriptFile('alerts.js', array('minimize' => true), 'smf_alerts');
5039
		}
5040
	}
5041
5042
	// All the buttons we can possible want and then some, try pulling the final list of buttons from cache first.
5043
	if (($menu_buttons = cache_get_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $cacheTime)) === null || time() - $cacheTime <= $modSettings['settings_updated'])
5044
	{
5045
		$buttons = array(
5046
			'home' => array(
5047
				'title' => $txt['home'],
5048
				'href' => $scripturl,
5049
				'show' => true,
5050
				'sub_buttons' => array(
5051
				),
5052
				'is_last' => $context['right_to_left'],
5053
			),
5054
			'search' => array(
5055
				'title' => $txt['search'],
5056
				'href' => $scripturl . '?action=search',
5057
				'show' => $context['allow_search'],
5058
				'sub_buttons' => array(
5059
				),
5060
			),
5061
			'admin' => array(
5062
				'title' => $txt['admin'],
5063
				'href' => $scripturl . '?action=admin',
5064
				'show' => $context['allow_admin'],
5065
				'sub_buttons' => array(
5066
					'featuresettings' => array(
5067
						'title' => $txt['modSettings_title'],
5068
						'href' => $scripturl . '?action=admin;area=featuresettings',
5069
						'show' => allowedTo('admin_forum'),
5070
					),
5071
					'packages' => array(
5072
						'title' => $txt['package'],
5073
						'href' => $scripturl . '?action=admin;area=packages',
5074
						'show' => allowedTo('admin_forum'),
5075
					),
5076
					'errorlog' => array(
5077
						'title' => $txt['errorlog'],
5078
						'href' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc',
5079
						'show' => allowedTo('admin_forum') && !empty($modSettings['enableErrorLogging']),
5080
					),
5081
					'permissions' => array(
5082
						'title' => $txt['edit_permissions'],
5083
						'href' => $scripturl . '?action=admin;area=permissions',
5084
						'show' => allowedTo('manage_permissions'),
5085
					),
5086
					'memberapprove' => array(
5087
						'title' => $txt['approve_members_waiting'],
5088
						'href' => $scripturl . '?action=admin;area=viewmembers;sa=browse;type=approve',
5089
						'show' => !empty($context['unapproved_members']),
5090
						'is_last' => true,
5091
					),
5092
				),
5093
			),
5094
			'moderate' => array(
5095
				'title' => $txt['moderate'],
5096
				'href' => $scripturl . '?action=moderate',
5097
				'show' => $context['allow_moderation_center'],
5098
				'sub_buttons' => array(
5099
					'modlog' => array(
5100
						'title' => $txt['modlog_view'],
5101
						'href' => $scripturl . '?action=moderate;area=modlog',
5102
						'show' => !empty($modSettings['modlog_enabled']) && !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
5103
					),
5104
					'poststopics' => array(
5105
						'title' => $txt['mc_unapproved_poststopics'],
5106
						'href' => $scripturl . '?action=moderate;area=postmod;sa=posts',
5107
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
5108
					),
5109
					'attachments' => array(
5110
						'title' => $txt['mc_unapproved_attachments'],
5111
						'href' => $scripturl . '?action=moderate;area=attachmod;sa=attachments',
5112
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
5113
					),
5114
					'reports' => array(
5115
						'title' => $txt['mc_reported_posts'],
5116
						'href' => $scripturl . '?action=moderate;area=reportedposts',
5117
						'show' => !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
5118
					),
5119
					'reported_members' => array(
5120
						'title' => $txt['mc_reported_members'],
5121
						'href' => $scripturl . '?action=moderate;area=reportedmembers',
5122
						'show' => allowedTo('moderate_forum'),
5123
						'is_last' => true,
5124
					)
5125
				),
5126
			),
5127
			'calendar' => array(
5128
				'title' => $txt['calendar'],
5129
				'href' => $scripturl . '?action=calendar',
5130
				'show' => $context['allow_calendar'],
5131
				'sub_buttons' => array(
5132
					'view' => array(
5133
						'title' => $txt['calendar_menu'],
5134
						'href' => $scripturl . '?action=calendar',
5135
						'show' => $context['allow_calendar_event'],
5136
					),
5137
					'post' => array(
5138
						'title' => $txt['calendar_post_event'],
5139
						'href' => $scripturl . '?action=calendar;sa=post',
5140
						'show' => $context['allow_calendar_event'],
5141
						'is_last' => true,
5142
					),
5143
				),
5144
			),
5145
			'mlist' => array(
5146
				'title' => $txt['members_title'],
5147
				'href' => $scripturl . '?action=mlist',
5148
				'show' => $context['allow_memberlist'],
5149
				'sub_buttons' => array(
5150
					'mlist_view' => array(
5151
						'title' => $txt['mlist_menu_view'],
5152
						'href' => $scripturl . '?action=mlist',
5153
						'show' => true,
5154
					),
5155
					'mlist_search' => array(
5156
						'title' => $txt['mlist_search'],
5157
						'href' => $scripturl . '?action=mlist;sa=search',
5158
						'show' => true,
5159
						'is_last' => true,
5160
					),
5161
				),
5162
				'is_last' => !$context['right_to_left'] && (!$user_info['is_guest'] || !$context['can_register']),
5163
			),
5164
			'signup' => array(
5165
				'title' => $txt['register'],
5166
				'href' => $scripturl . '?action=signup',
5167
				'show' => $user_info['is_guest'] && $context['can_register'],
5168
				'sub_buttons' => array(
5169
				),
5170
				'is_last' => !$context['right_to_left'],
5171
			),
5172
		);
5173
5174
		// Allow editing menu buttons easily.
5175
		call_integration_hook('integrate_menu_buttons', array(&$buttons));
5176
5177
		// Now we put the buttons in the context so the theme can use them.
5178
		$menu_buttons = array();
5179
		foreach ($buttons as $act => $button)
5180
			if (!empty($button['show']))
5181
			{
5182
				$button['active_button'] = false;
5183
5184
				// This button needs some action.
5185
				if (isset($button['action_hook']))
5186
					$needs_action_hook = true;
5187
5188
				// Make sure the last button truly is the last button.
5189
				if (!empty($button['is_last']))
5190
				{
5191
					if (isset($last_button))
5192
						unset($menu_buttons[$last_button]['is_last']);
5193
					$last_button = $act;
5194
				}
5195
5196
				// Go through the sub buttons if there are any.
5197
				if (!empty($button['sub_buttons']))
5198
					foreach ($button['sub_buttons'] as $key => $subbutton)
5199
					{
5200
						if (empty($subbutton['show']))
5201
							unset($button['sub_buttons'][$key]);
5202
5203
						// 2nd level sub buttons next...
5204
						if (!empty($subbutton['sub_buttons']))
5205
						{
5206
							foreach ($subbutton['sub_buttons'] as $key2 => $sub_button2)
5207
							{
5208
								if (empty($sub_button2['show']))
5209
									unset($button['sub_buttons'][$key]['sub_buttons'][$key2]);
5210
							}
5211
						}
5212
					}
5213
5214
				// Does this button have its own icon?
5215
				if (isset($button['icon']) && file_exists($settings['theme_dir'] . '/images/' . $button['icon']))
5216
					$button['icon'] = '<img src="' . $settings['images_url'] . '/' . $button['icon'] . '" alt="">';
5217
				elseif (isset($button['icon']) && file_exists($settings['default_theme_dir'] . '/images/' . $button['icon']))
5218
					$button['icon'] = '<img src="' . $settings['default_images_url'] . '/' . $button['icon'] . '" alt="">';
5219
				elseif (isset($button['icon']))
5220
					$button['icon'] = '<span class="main_icons ' . $button['icon'] . '"></span>';
5221
				else
5222
					$button['icon'] = '<span class="main_icons ' . $act . '"></span>';
5223
5224
				$menu_buttons[$act] = $button;
5225
			}
5226
5227
		if (!empty($cache_enable) && $cache_enable >= 2)
5228
			cache_put_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $menu_buttons, $cacheTime);
5229
	}
5230
5231
	$context['menu_buttons'] = $menu_buttons;
5232
5233
	// Logging out requires the session id in the url.
5234
	if (isset($context['menu_buttons']['logout']))
5235
		$context['menu_buttons']['logout']['href'] = sprintf($context['menu_buttons']['logout']['href'], $context['session_var'], $context['session_id']);
5236
5237
	// Figure out which action we are doing so we can set the active tab.
5238
	// Default to home.
5239
	$current_action = 'home';
5240
5241
	if (isset($context['menu_buttons'][$context['current_action']]))
5242
		$current_action = $context['current_action'];
5243
	elseif ($context['current_action'] == 'search2')
5244
		$current_action = 'search';
5245
	elseif ($context['current_action'] == 'theme')
5246
		$current_action = isset($_REQUEST['sa']) && $_REQUEST['sa'] == 'pick' ? 'profile' : 'admin';
5247
	elseif ($context['current_action'] == 'register2')
5248
		$current_action = 'register';
5249
	elseif ($context['current_action'] == 'login2' || ($user_info['is_guest'] && $context['current_action'] == 'reminder'))
5250
		$current_action = 'login';
5251
	elseif ($context['current_action'] == 'groups' && $context['allow_moderation_center'])
5252
		$current_action = 'moderate';
5253
5254
	// There are certain exceptions to the above where we don't want anything on the menu highlighted.
5255
	if ($context['current_action'] == 'profile' && !empty($context['user']['is_owner']))
5256
	{
5257
		$current_action = !empty($_GET['area']) && $_GET['area'] == 'showalerts' ? 'self_alerts' : 'self_profile';
5258
		$context[$current_action] = true;
5259
	}
5260
	elseif ($context['current_action'] == 'pm')
5261
	{
5262
		$current_action = 'self_pm';
5263
		$context['self_pm'] = true;
5264
	}
5265
5266
	$context['total_mod_reports'] = 0;
5267
	$context['total_admin_reports'] = 0;
5268
5269
	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']))
5270
	{
5271
		$context['total_mod_reports'] = $context['open_mod_reports'];
5272
		$context['menu_buttons']['moderate']['sub_buttons']['reports']['amt'] = $context['open_mod_reports'];
5273
	}
5274
5275
	// Show how many errors there are
5276
	if (!empty($context['menu_buttons']['admin']['sub_buttons']['errorlog']))
5277
	{
5278
		// Get an error count, if necessary
5279
		if (!isset($context['num_errors']))
5280
		{
5281
			$query = $smcFunc['db_query']('', '
5282
				SELECT COUNT(*)
5283
				FROM {db_prefix}log_errors',
5284
				array()
5285
			);
5286
5287
			list($context['num_errors']) = $smcFunc['db_fetch_row']($query);
5288
			$smcFunc['db_free_result']($query);
5289
		}
5290
5291
		if (!empty($context['num_errors']))
5292
		{
5293
			$context['total_admin_reports'] += $context['num_errors'];
5294
			$context['menu_buttons']['admin']['sub_buttons']['errorlog']['amt'] = $context['num_errors'];
5295
		}
5296
	}
5297
5298
	// Show number of reported members
5299
	if (!empty($context['open_member_reports']) && !empty($context['menu_buttons']['moderate']['sub_buttons']['reported_members']))
5300
	{
5301
		$context['total_mod_reports'] += $context['open_member_reports'];
5302
		$context['menu_buttons']['moderate']['sub_buttons']['reported_members']['amt'] = $context['open_member_reports'];
5303
	}
5304
5305
	if (!empty($context['unapproved_members']) && !empty($context['menu_buttons']['admin']))
5306
	{
5307
		$context['menu_buttons']['admin']['sub_buttons']['memberapprove']['amt'] = $context['unapproved_members'];
5308
		$context['total_admin_reports'] += $context['unapproved_members'];
5309
	}
5310
5311
	if ($context['total_admin_reports'] > 0 && !empty($context['menu_buttons']['admin']))
5312
	{
5313
		$context['menu_buttons']['admin']['amt'] = $context['total_admin_reports'];
5314
	}
5315
5316
	// Do we have any open reports?
5317
	if ($context['total_mod_reports'] > 0 && !empty($context['menu_buttons']['moderate']))
5318
	{
5319
		$context['menu_buttons']['moderate']['amt'] = $context['total_mod_reports'];
5320
	}
5321
5322
	// Not all actions are simple.
5323
	if (!empty($needs_action_hook))
5324
		call_integration_hook('integrate_current_action', array(&$current_action));
5325
5326
	if (isset($context['menu_buttons'][$current_action]))
5327
		$context['menu_buttons'][$current_action]['active_button'] = true;
5328
}
5329
5330
/**
5331
 * Generate a random seed and ensure it's stored in settings.
5332
 */
5333
function smf_seed_generator()
5334
{
5335
	updateSettings(array('rand_seed' => microtime(true)));
5336
}
5337
5338
/**
5339
 * Process functions of an integration hook.
5340
 * calls all functions of the given hook.
5341
 * supports static class method calls.
5342
 *
5343
 * @param string $hook The hook name
5344
 * @param array $parameters An array of parameters this hook implements
5345
 * @return array The results of the functions
5346
 */
5347
function call_integration_hook($hook, $parameters = array())
5348
{
5349
	global $modSettings, $settings, $boarddir, $sourcedir, $db_show_debug;
5350
	global $context, $txt;
5351
5352
	if ($db_show_debug === true)
5353
		$context['debug']['hooks'][] = $hook;
5354
5355
	// Need to have some control.
5356
	if (!isset($context['instances']))
5357
		$context['instances'] = array();
5358
5359
	$results = array();
5360
	if (empty($modSettings[$hook]))
5361
		return $results;
5362
5363
	$functions = explode(',', $modSettings[$hook]);
5364
	// Loop through each function.
5365
	foreach ($functions as $function)
5366
	{
5367
		// Hook has been marked as "disabled". Skip it!
5368
		if (strpos($function, '!') !== false)
5369
			continue;
5370
5371
		$call = call_helper($function, true);
5372
5373
		// Is it valid?
5374
		if (!empty($call))
5375
			$results[$function] = call_user_func_array($call, $parameters);
0 ignored issues
show
Bug introduced by
It seems like $call can also be of type boolean; however, parameter $callback of call_user_func_array() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

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

5375
			$results[$function] = call_user_func_array(/** @scrutinizer ignore-type */ $call, $parameters);
Loading history...
5376
		// This failed, but we want to do so silently.
5377
		elseif (!empty($function) && !empty($context['ignore_hook_errors']))
5378
			return $results;
5379
		// Whatever it was suppose to call, it failed :(
5380
		elseif (!empty($function))
5381
		{
5382
			loadLanguage('Errors');
5383
5384
			// Get a full path to show on error.
5385
			if (strpos($function, '|') !== false)
5386
			{
5387
				list ($file, $string) = explode('|', $function);
5388
				$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'])));
5389
				log_error(sprintf($txt['hook_fail_call_to'], $string, $absPath), 'general');
5390
			}
5391
			// "Assume" the file resides on $boarddir somewhere...
5392
			else
5393
				log_error(sprintf($txt['hook_fail_call_to'], $function, $boarddir), 'general');
5394
		}
5395
	}
5396
5397
	return $results;
5398
}
5399
5400
/**
5401
 * Add a function for integration hook.
5402
 * does nothing if the function is already added.
5403
 *
5404
 * @param string $hook The complete hook name.
5405
 * @param string $function The function name. Can be a call to a method via Class::method.
5406
 * @param bool $permanent If true, updates the value in settings table.
5407
 * @param string $file The file. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
5408
 * @param bool $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
5409
 */
5410
function add_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
5411
{
5412
	global $smcFunc, $modSettings;
5413
5414
	// Any objects?
5415
	if ($object)
5416
		$function = $function . '#';
5417
5418
	// Any files  to load?
5419
	if (!empty($file) && is_string($file))
5420
		$function = $file . (!empty($function) ? '|' . $function : '');
5421
5422
	// Get the correct string.
5423
	$integration_call = $function;
5424
5425
	// Is it going to be permanent?
5426
	if ($permanent)
5427
	{
5428
		$request = $smcFunc['db_query']('', '
5429
			SELECT value
5430
			FROM {db_prefix}settings
5431
			WHERE variable = {string:variable}',
5432
			array(
5433
				'variable' => $hook,
5434
			)
5435
		);
5436
		list ($current_functions) = $smcFunc['db_fetch_row']($request);
5437
		$smcFunc['db_free_result']($request);
5438
5439
		if (!empty($current_functions))
5440
		{
5441
			$current_functions = explode(',', $current_functions);
5442
			if (in_array($integration_call, $current_functions))
5443
				return;
5444
5445
			$permanent_functions = array_merge($current_functions, array($integration_call));
5446
		}
5447
		else
5448
			$permanent_functions = array($integration_call);
5449
5450
		updateSettings(array($hook => implode(',', $permanent_functions)));
5451
	}
5452
5453
	// Make current function list usable.
5454
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
5455
5456
	// Do nothing, if it's already there.
5457
	if (in_array($integration_call, $functions))
5458
		return;
5459
5460
	$functions[] = $integration_call;
5461
	$modSettings[$hook] = implode(',', $functions);
5462
}
5463
5464
/**
5465
 * Remove an integration hook function.
5466
 * Removes the given function from the given hook.
5467
 * Does nothing if the function is not available.
5468
 *
5469
 * @param string $hook The complete hook name.
5470
 * @param string $function The function name. Can be a call to a method via Class::method.
5471
 * @param boolean $permanent Irrelevant for the function itself but need to declare it to match
5472
 * @param string $file The filename. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
5473
 * @param boolean $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
5474
 * @see add_integration_function
5475
 */
5476
function remove_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
5477
{
5478
	global $smcFunc, $modSettings;
5479
5480
	// Any objects?
5481
	if ($object)
5482
		$function = $function . '#';
5483
5484
	// Any files  to load?
5485
	if (!empty($file) && is_string($file))
5486
		$function = $file . '|' . $function;
5487
5488
	// Get the correct string.
5489
	$integration_call = $function;
5490
5491
	// Get the permanent functions.
5492
	$request = $smcFunc['db_query']('', '
5493
		SELECT value
5494
		FROM {db_prefix}settings
5495
		WHERE variable = {string:variable}',
5496
		array(
5497
			'variable' => $hook,
5498
		)
5499
	);
5500
	list ($current_functions) = $smcFunc['db_fetch_row']($request);
5501
	$smcFunc['db_free_result']($request);
5502
5503
	if (!empty($current_functions))
5504
	{
5505
		$current_functions = explode(',', $current_functions);
5506
5507
		if (in_array($integration_call, $current_functions))
5508
			updateSettings(array($hook => implode(',', array_diff($current_functions, array($integration_call)))));
5509
	}
5510
5511
	// Turn the function list into something usable.
5512
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
5513
5514
	// You can only remove it if it's available.
5515
	if (!in_array($integration_call, $functions))
5516
		return;
5517
5518
	$functions = array_diff($functions, array($integration_call));
5519
	$modSettings[$hook] = implode(',', $functions);
5520
}
5521
5522
/**
5523
 * Receives a string and tries to figure it out if its a method or a function.
5524
 * If a method is found, it looks for a "#" which indicates SMF should create a new instance of the given class.
5525
 * Checks the string/array for is_callable() and return false/fatal_lang_error is the given value results in a non callable string/array.
5526
 * Prepare and returns a callable depending on the type of method/function found.
5527
 *
5528
 * @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)
5529
 * @param boolean $return If true, the function will not call the function/method but instead will return the formatted string.
5530
 * @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.
5531
 */
5532
function call_helper($string, $return = false)
5533
{
5534
	global $context, $smcFunc, $txt, $db_show_debug;
5535
5536
	// Really?
5537
	if (empty($string))
5538
		return false;
5539
5540
	// An array? should be a "callable" array IE array(object/class, valid_callable).
5541
	// A closure? should be a callable one.
5542
	if (is_array($string) || $string instanceof Closure)
5543
		return $return ? $string : (is_callable($string) ? call_user_func($string) : false);
5544
5545
	// No full objects, sorry! pass a method or a property instead!
5546
	if (is_object($string))
5547
		return false;
5548
5549
	// Stay vitaminized my friends...
5550
	$string = $smcFunc['htmlspecialchars']($smcFunc['htmltrim']($string));
5551
5552
	// Is there a file to load?
5553
	$string = load_file($string);
5554
5555
	// Loaded file failed
5556
	if (empty($string))
5557
		return false;
5558
5559
	// Found a method.
5560
	if (strpos($string, '::') !== false)
0 ignored issues
show
Bug introduced by
It seems like $string can also be of type boolean; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

5562
		list ($class, $method) = explode('::', /** @scrutinizer ignore-type */ $string);
Loading history...
5563
5564
		// Check if a new object will be created.
5565
		if (strpos($method, '#') !== false)
5566
		{
5567
			// Need to remove the # thing.
5568
			$method = str_replace('#', '', $method);
5569
5570
			// Don't need to create a new instance for every method.
5571
			if (empty($context['instances'][$class]) || !($context['instances'][$class] instanceof $class))
5572
			{
5573
				$context['instances'][$class] = new $class;
5574
5575
				// Add another one to the list.
5576
				if ($db_show_debug === true)
5577
				{
5578
					if (!isset($context['debug']['instances']))
5579
						$context['debug']['instances'] = array();
5580
5581
					$context['debug']['instances'][$class] = $class;
5582
				}
5583
			}
5584
5585
			$func = array($context['instances'][$class], $method);
5586
		}
5587
5588
		// Right then. This is a call to a static method.
5589
		else
5590
			$func = array($class, $method);
5591
	}
5592
5593
	// Nope! just a plain regular function.
5594
	else
5595
		$func = $string;
5596
5597
	// We can't call this helper, but we want to silently ignore this.
5598
	if (!is_callable($func, false, $callable_name) && !empty($context['ignore_hook_errors']))
5599
		return false;
5600
5601
	// Right, we got what we need, time to do some checks.
5602
	elseif (!is_callable($func, false, $callable_name))
5603
	{
5604
		loadLanguage('Errors');
5605
		log_error(sprintf($txt['sub_action_fail'], $callable_name), 'general');
5606
5607
		// Gotta tell everybody.
5608
		return false;
5609
	}
5610
5611
	// Everything went better than expected.
5612
	else
5613
	{
5614
		// What are we gonna do about it?
5615
		if ($return)
5616
			return $func;
5617
5618
		// If this is a plain function, avoid the heat of calling call_user_func().
5619
		else
5620
		{
5621
			if (is_array($func))
5622
				call_user_func($func);
5623
5624
			else
5625
				$func();
5626
		}
5627
	}
5628
}
5629
5630
/**
5631
 * Receives a string and tries to figure it out if it contains info to load a file.
5632
 * Checks for a | (pipe) symbol and tries to load a file with the info given.
5633
 * 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.
5634
 *
5635
 * @param string $string The string containing a valid format.
5636
 * @return string|boolean The given string with the pipe and file info removed. Boolean false if the file couldn't be loaded.
5637
 */
5638
function load_file($string)
5639
{
5640
	global $sourcedir, $txt, $boarddir, $settings, $context;
5641
5642
	if (empty($string))
5643
		return false;
5644
5645
	if (strpos($string, '|') !== false)
5646
	{
5647
		list ($file, $string) = explode('|', $string);
5648
5649
		// Match the wildcards to their regular vars.
5650
		if (empty($settings['theme_dir']))
5651
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
5652
5653
		else
5654
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
5655
5656
		// Load the file if it can be loaded.
5657
		if (file_exists($absPath))
5658
			require_once($absPath);
5659
5660
		// No? try a fallback to $sourcedir
5661
		else
5662
		{
5663
			$absPath = $sourcedir . '/' . $file;
5664
5665
			if (file_exists($absPath))
5666
				require_once($absPath);
5667
5668
			// Sorry, can't do much for you at this point.
5669
			elseif (empty($context['uninstalling']))
5670
			{
5671
				loadLanguage('Errors');
5672
				log_error(sprintf($txt['hook_fail_loading_file'], $absPath), 'general');
5673
5674
				// File couldn't be loaded.
5675
				return false;
5676
			}
5677
		}
5678
	}
5679
5680
	return $string;
5681
}
5682
5683
/**
5684
 * Get the contents of a URL, irrespective of allow_url_fopen.
5685
 *
5686
 * - reads the contents of an http or ftp address and returns the page in a string
5687
 * - will accept up to 3 page redirections (redirectio_level in the function call is private)
5688
 * - if post_data is supplied, the value and length is posted to the given url as form data
5689
 * - URL must be supplied in lowercase
5690
 *
5691
 * @param string $url The URL
5692
 * @param string $post_data The data to post to the given URL
5693
 * @param bool $keep_alive Whether to send keepalive info
5694
 * @param int $redirection_level How many levels of redirection
5695
 * @return string|false The fetched data or false on failure
5696
 */
5697
function fetch_web_data($url, $post_data = '', $keep_alive = false, $redirection_level = 0)
5698
{
5699
	global $webmaster_email, $sourcedir, $txt;
5700
	static $keep_alive_dom = null, $keep_alive_fp = null;
5701
5702
	preg_match('~^(http|ftp)(s)?://([^/:]+)(:(\d+))?(.+)$~', $url, $match);
5703
5704
	// No scheme? No data for you!
5705
	if (empty($match[1]))
5706
		return false;
5707
5708
	// An FTP url. We should try connecting and RETRieving it...
5709
	elseif ($match[1] == 'ftp')
5710
	{
5711
		// Include the file containing the ftp_connection class.
5712
		require_once($sourcedir . '/Class-Package.php');
5713
5714
		// Establish a connection and attempt to enable passive mode.
5715
		$ftp = new ftp_connection(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? 21 : $match[5], 'anonymous', $webmaster_email);
5716
		if ($ftp->error !== false || !$ftp->passive())
0 ignored issues
show
introduced by
The condition $ftp->error !== false is always true.
Loading history...
5717
			return false;
5718
5719
		// I want that one *points*!
5720
		fwrite($ftp->connection, 'RETR ' . $match[6] . "\r\n");
5721
5722
		// Since passive mode worked (or we would have returned already!) open the connection.
5723
		$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...
5724
		if (!$fp)
5725
			return false;
5726
5727
		// The server should now say something in acknowledgement.
5728
		$ftp->check_response(150);
5729
5730
		$data = '';
5731
		while (!feof($fp))
5732
			$data .= fread($fp, 4096);
5733
		fclose($fp);
5734
5735
		// All done, right?  Good.
5736
		$ftp->check_response(226);
5737
		$ftp->close();
5738
	}
5739
5740
	// This is more likely; a standard HTTP URL.
5741
	elseif (isset($match[1]) && $match[1] == 'http')
5742
	{
5743
		// First try to use fsockopen, because it is fastest.
5744
		if ($keep_alive && $match[3] == $keep_alive_dom)
5745
			$fp = $keep_alive_fp;
5746
		if (empty($fp))
5747
		{
5748
			// Open the socket on the port we want...
5749
			$fp = @fsockopen(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? ($match[2] ? 443 : 80) : $match[5], $err, $err, 5);
5750
		}
5751
		if (!empty($fp))
5752
		{
5753
			if ($keep_alive)
5754
			{
5755
				$keep_alive_dom = $match[3];
5756
				$keep_alive_fp = $fp;
5757
			}
5758
5759
			// I want this, from there, and I'm not going to be bothering you for more (probably.)
5760
			if (empty($post_data))
5761
			{
5762
				fwrite($fp, 'GET ' . ($match[6] !== '/' ? str_replace(' ', '%20', $match[6]) : '') . ' HTTP/1.0' . "\r\n");
5763
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
5764
				fwrite($fp, 'user-agent: '. SMF_USER_AGENT . "\r\n");
5765
				if ($keep_alive)
5766
					fwrite($fp, 'connection: Keep-Alive' . "\r\n\r\n");
5767
				else
5768
					fwrite($fp, 'connection: close' . "\r\n\r\n");
5769
			}
5770
			else
5771
			{
5772
				fwrite($fp, 'POST ' . ($match[6] !== '/' ? $match[6] : '') . ' HTTP/1.0' . "\r\n");
5773
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
5774
				fwrite($fp, 'user-agent: '. SMF_USER_AGENT . "\r\n");
5775
				if ($keep_alive)
5776
					fwrite($fp, 'connection: Keep-Alive' . "\r\n");
5777
				else
5778
					fwrite($fp, 'connection: close' . "\r\n");
5779
				fwrite($fp, 'content-type: application/x-www-form-urlencoded' . "\r\n");
5780
				fwrite($fp, 'content-length: ' . strlen($post_data) . "\r\n\r\n");
5781
				fwrite($fp, $post_data);
5782
			}
5783
5784
			$response = fgets($fp, 768);
5785
5786
			// Redirect in case this location is permanently or temporarily moved.
5787
			if ($redirection_level < 3 && preg_match('~^HTTP/\S+\s+30[127]~i', $response) === 1)
5788
			{
5789
				$header = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $header is dead and can be removed.
Loading history...
5790
				$location = '';
5791
				while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
5792
					if (stripos($header, 'location:') !== false)
5793
						$location = trim(substr($header, strpos($header, ':') + 1));
5794
5795
				if (empty($location))
5796
					return false;
5797
				else
5798
				{
5799
					if (!$keep_alive)
5800
						fclose($fp);
5801
					return fetch_web_data($location, $post_data, $keep_alive, $redirection_level + 1);
5802
				}
5803
			}
5804
5805
			// Make sure we get a 200 OK.
5806
			elseif (preg_match('~^HTTP/\S+\s+20[01]~i', $response) === 0)
5807
				return false;
5808
5809
			// Skip the headers...
5810
			while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
5811
			{
5812
				if (preg_match('~content-length:\s*(\d+)~i', $header, $match) != 0)
5813
					$content_length = $match[1];
5814
				elseif (preg_match('~connection:\s*close~i', $header) != 0)
5815
				{
5816
					$keep_alive_dom = null;
5817
					$keep_alive = false;
5818
				}
5819
5820
				continue;
5821
			}
5822
5823
			$data = '';
5824
			if (isset($content_length))
5825
			{
5826
				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...
5827
					$data .= fread($fp, $content_length - strlen($data));
5828
			}
5829
			else
5830
			{
5831
				while (!feof($fp))
5832
					$data .= fread($fp, 4096);
5833
			}
5834
5835
			if (!$keep_alive)
5836
				fclose($fp);
5837
		}
5838
5839
		// If using fsockopen didn't work, try to use cURL if available.
5840
		elseif (function_exists('curl_init'))
5841
		{
5842
			// Include the file containing the curl_fetch_web_data class.
5843
			require_once($sourcedir . '/Class-CurlFetchWeb.php');
5844
5845
			$fetch_data = new curl_fetch_web_data();
5846
			$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

5846
			$fetch_data->get_url_data($url, /** @scrutinizer ignore-type */ $post_data);
Loading history...
5847
5848
			// no errors and a 200 result, then we have a good dataset, well we at least have data. ;)
5849
			if ($fetch_data->result('code') == 200 && !$fetch_data->result('error'))
5850
				$data = $fetch_data->result('body');
5851
			else
5852
				return false;
5853
		}
5854
5855
		// Neither fsockopen nor curl are available. Well, phooey.
5856
		else
5857
			return false;
5858
	}
5859
	else
5860
	{
5861
		// Umm, this shouldn't happen?
5862
		loadLanguage('Errors');
5863
		trigger_error($txt['fetch_web_data_bad_url'], E_USER_NOTICE);
5864
		$data = false;
5865
	}
5866
5867
	return $data;
5868
}
5869
5870
/**
5871
 * Attempts to determine the MIME type of some data or a file.
5872
 *
5873
 * @param string $data The data to check, or the path or URL of a file to check.
5874
 * @param string $is_path If true, $data is a path or URL to a file.
5875
 * @return string|bool A MIME type, or false if we cannot determine it.
5876
 */
5877
function get_mime_type($data, $is_path = false)
5878
{
5879
	global $cachedir;
5880
5881
	$finfo_loaded = extension_loaded('fileinfo');
5882
	$exif_loaded = extension_loaded('exif') && function_exists('image_type_to_mime_type');
5883
5884
	// Oh well. We tried.
5885
	if (!$finfo_loaded && !$exif_loaded)
5886
		return false;
5887
5888
	// Start with the 'empty' MIME type.
5889
	$mime_type = 'application/x-empty';
5890
5891
	if ($finfo_loaded)
5892
	{
5893
		// Just some nice, simple data to analyze.
5894
		if (empty($is_path))
5895
			$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
5896
5897
		// A file, or maybe a URL?
5898
		else
5899
		{
5900
			// Local file.
5901
			if (file_exists($data))
5902
				$mime_type = mime_content_type($data);
5903
5904
			// URL.
5905
			elseif ($data = fetch_web_data($data))
5906
				$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
5907
		}
5908
	}
5909
	// Workaround using Exif requires a local file.
5910
	else
5911
	{
5912
		// If $data is a URL to fetch, do so.
5913
		if (!empty($is_path) && !file_exists($data) && url_exists($data))
5914
		{
5915
			$data = fetch_web_data($data);
5916
			$is_path = false;
5917
		}
5918
5919
		// If we don't have a local file, create one and use it.
5920
		if (empty($is_path))
5921
		{
5922
			$temp_file = tempnam($cachedir, md5($data));
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type false; however, parameter $string of md5() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

5922
			$temp_file = tempnam($cachedir, md5(/** @scrutinizer ignore-type */ $data));
Loading history...
5923
			file_put_contents($temp_file, $data);
5924
			$is_path = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $is_path is dead and can be removed.
Loading history...
5925
			$data = $temp_file;
5926
		}
5927
5928
		$imagetype = @exif_imagetype($data);
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type false; however, parameter $filename of exif_imagetype() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

5928
		$imagetype = @exif_imagetype(/** @scrutinizer ignore-type */ $data);
Loading history...
5929
5930
		if (isset($temp_file))
5931
			unlink($temp_file);
5932
5933
		// Unfortunately, this workaround only works for image files.
5934
		if ($imagetype !== false)
5935
			$mime_type = image_type_to_mime_type($imagetype);
5936
	}
5937
5938
	return $mime_type;
5939
}
5940
5941
/**
5942
 * Checks whether a file or data has the expected MIME type.
5943
 *
5944
 * @param string $data The data to check, or the path or URL of a file to check.
5945
 * @param string $type_pattern A regex pattern to match the acceptable MIME types.
5946
 * @param string $is_path If true, $data is a path or URL to a file.
5947
 * @return int 1 if the detected MIME type matches the pattern, 0 if it doesn't, or 2 if we can't check.
5948
 */
5949
function check_mime_type($data, $type_pattern, $is_path = false)
5950
{
5951
	// Get the MIME type.
5952
	$mime_type = get_mime_type($data, $is_path);
0 ignored issues
show
Bug introduced by
It seems like $is_path can also be of type false; however, parameter $is_path of get_mime_type() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

5952
	$mime_type = get_mime_type($data, /** @scrutinizer ignore-type */ $is_path);
Loading history...
5953
5954
	// Couldn't determine it.
5955
	if ($mime_type === false)
5956
		return 2;
5957
5958
	// Check whether the MIME type matches expectations.
5959
	return (int) @preg_match('~' . $type_pattern . '~', $mime_type);
5960
}
5961
5962
/**
5963
 * Prepares an array of "likes" info for the topic specified by $topic
5964
 *
5965
 * @param integer $topic The topic ID to fetch the info from.
5966
 * @return array An array of IDs of messages in the specified topic that the current user likes
5967
 */
5968
function prepareLikesContext($topic)
5969
{
5970
	global $user_info, $smcFunc;
5971
5972
	// Make sure we have something to work with.
5973
	if (empty($topic))
5974
		return array();
5975
5976
	// We already know the number of likes per message, we just want to know whether the current user liked it or not.
5977
	$user = $user_info['id'];
5978
	$cache_key = 'likes_topic_' . $topic . '_' . $user;
5979
	$ttl = 180;
5980
5981
	if (($temp = cache_get_data($cache_key, $ttl)) === null)
5982
	{
5983
		$temp = array();
5984
		$request = $smcFunc['db_query']('', '
5985
			SELECT content_id
5986
			FROM {db_prefix}user_likes AS l
5987
				INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg)
5988
			WHERE l.id_member = {int:current_user}
5989
				AND l.content_type = {literal:msg}
5990
				AND m.id_topic = {int:topic}',
5991
			array(
5992
				'current_user' => $user,
5993
				'topic' => $topic,
5994
			)
5995
		);
5996
		while ($row = $smcFunc['db_fetch_assoc']($request))
5997
			$temp[] = (int) $row['content_id'];
5998
5999
		cache_put_data($cache_key, $temp, $ttl);
6000
	}
6001
6002
	return $temp;
6003
}
6004
6005
/**
6006
 * Microsoft uses their own character set Code Page 1252 (CP1252), which is a
6007
 * superset of ISO 8859-1, defining several characters between DEC 128 and 159
6008
 * that are not normally displayable.  This converts the popular ones that
6009
 * appear from a cut and paste from windows.
6010
 *
6011
 * @param string $string The string
6012
 * @return string The sanitized string
6013
 */
6014
function sanitizeMSCutPaste($string)
6015
{
6016
	global $context;
6017
6018
	if (empty($string))
6019
		return $string;
6020
6021
	// UTF-8 occurences of MS special characters
6022
	$findchars_utf8 = array(
6023
		"\xe2\x80\x9a",	// single low-9 quotation mark
6024
		"\xe2\x80\x9e",	// double low-9 quotation mark
6025
		"\xe2\x80\xa6",	// horizontal ellipsis
6026
		"\xe2\x80\x98",	// left single curly quote
6027
		"\xe2\x80\x99",	// right single curly quote
6028
		"\xe2\x80\x9c",	// left double curly quote
6029
		"\xe2\x80\x9d",	// right double curly quote
6030
	);
6031
6032
	// windows 1252 / iso equivalents
6033
	$findchars_iso = array(
6034
		chr(130),
6035
		chr(132),
6036
		chr(133),
6037
		chr(145),
6038
		chr(146),
6039
		chr(147),
6040
		chr(148),
6041
	);
6042
6043
	// safe replacements
6044
	$replacechars = array(
6045
		',',	// &sbquo;
6046
		',,',	// &bdquo;
6047
		'...',	// &hellip;
6048
		"'",	// &lsquo;
6049
		"'",	// &rsquo;
6050
		'"',	// &ldquo;
6051
		'"',	// &rdquo;
6052
	);
6053
6054
	if ($context['utf8'])
6055
		$string = str_replace($findchars_utf8, $replacechars, $string);
6056
	else
6057
		$string = str_replace($findchars_iso, $replacechars, $string);
6058
6059
	return $string;
6060
}
6061
6062
/**
6063
 * Decode numeric html entities to their ascii or UTF8 equivalent character.
6064
 *
6065
 * Callback function for preg_replace_callback in subs-members
6066
 * Uses capture group 2 in the supplied array
6067
 * Does basic scan to ensure characters are inside a valid range
6068
 *
6069
 * @param array $matches An array of matches (relevant info should be the 3rd item)
6070
 * @return string A fixed string
6071
 */
6072
function replaceEntities__callback($matches)
6073
{
6074
	global $context;
6075
6076
	if (!isset($matches[2]))
6077
		return '';
6078
6079
	$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

6079
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
6080
6081
	// remove left to right / right to left overrides
6082
	if ($num === 0x202D || $num === 0x202E)
6083
		return '';
6084
6085
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
6086
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
6087
		return '&#' . $num . ';';
6088
6089
	if (empty($context['utf8']))
6090
	{
6091
		// no control characters
6092
		if ($num < 0x20)
6093
			return '';
6094
		// text is text
6095
		elseif ($num < 0x80)
6096
			return chr($num);
0 ignored issues
show
Bug introduced by
It seems like $num can also be of type double; however, parameter $codepoint of chr() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

6096
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
6097
		// all others get html-ised
6098
		else
6099
			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

6099
			return '&#' . /** @scrutinizer ignore-type */ $matches[2] . ';';
Loading history...
6100
	}
6101
	else
6102
	{
6103
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
6104
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
6105
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
6106
			return '';
6107
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
6108
		elseif ($num < 0x80)
6109
			return chr($num);
6110
		// <0x800 (2048)
6111
		elseif ($num < 0x800)
6112
			return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
6113
		// < 0x10000 (65536)
6114
		elseif ($num < 0x10000)
6115
			return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6116
		// <= 0x10FFFF (1114111)
6117
		else
6118
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6119
	}
6120
}
6121
6122
/**
6123
 * Converts html entities to utf8 equivalents
6124
 *
6125
 * Callback function for preg_replace_callback
6126
 * Uses capture group 1 in the supplied array
6127
 * Does basic checks to keep characters inside a viewable range.
6128
 *
6129
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
6130
 * @return string The fixed string
6131
 */
6132
function fixchar__callback($matches)
6133
{
6134
	if (!isset($matches[1]))
6135
		return '';
6136
6137
	$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

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

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

6145
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
6146
	// <0x800 (2048)
6147
	elseif ($num < 0x800)
6148
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
6149
	// < 0x10000 (65536)
6150
	elseif ($num < 0x10000)
6151
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6152
	// <= 0x10FFFF (1114111)
6153
	else
6154
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6155
}
6156
6157
/**
6158
 * Strips out invalid html entities, replaces others with html style &#123; codes
6159
 *
6160
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
6161
 * strpos, strlen, substr etc
6162
 *
6163
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
6164
 * @return string The fixed string
6165
 */
6166
function entity_fix__callback($matches)
6167
{
6168
	if (!isset($matches[2]))
6169
		return '';
6170
6171
	$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

6171
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
6172
6173
	// we don't allow control characters, characters out of range, byte markers, etc
6174
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
6175
		return '';
6176
	else
6177
		return '&#' . $num . ';';
6178
}
6179
6180
/**
6181
 * Return a Gravatar URL based on
6182
 * - the supplied email address,
6183
 * - the global maximum rating,
6184
 * - the global default fallback,
6185
 * - maximum sizes as set in the admin panel.
6186
 *
6187
 * It is SSL aware, and caches most of the parameters.
6188
 *
6189
 * @param string $email_address The user's email address
6190
 * @return string The gravatar URL
6191
 */
6192
function get_gravatar_url($email_address)
6193
{
6194
	global $modSettings, $smcFunc;
6195
	static $url_params = null;
6196
6197
	if ($url_params === null)
6198
	{
6199
		$ratings = array('G', 'PG', 'R', 'X');
6200
		$defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank');
6201
		$url_params = array();
6202
		if (!empty($modSettings['gravatarMaxRating']) && in_array($modSettings['gravatarMaxRating'], $ratings))
6203
			$url_params[] = 'rating=' . $modSettings['gravatarMaxRating'];
6204
		if (!empty($modSettings['gravatarDefault']) && in_array($modSettings['gravatarDefault'], $defaults))
6205
			$url_params[] = 'default=' . $modSettings['gravatarDefault'];
6206
		if (!empty($modSettings['avatar_max_width_external']))
6207
			$size_string = (int) $modSettings['avatar_max_width_external'];
6208
		if (!empty($modSettings['avatar_max_height_external']) && !empty($size_string))
6209
			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...
6210
				$size_string = $modSettings['avatar_max_height_external'];
6211
6212
		if (!empty($size_string))
6213
			$url_params[] = 's=' . $size_string;
6214
	}
6215
	$http_method = !empty($modSettings['force_ssl']) ? 'https://secure' : 'http://www';
6216
6217
	return $http_method . '.gravatar.com/avatar/' . md5($smcFunc['strtolower']($email_address)) . '?' . implode('&', $url_params);
6218
}
6219
6220
/**
6221
 * Get a list of time zones.
6222
 *
6223
 * @param string $when The date/time for which to calculate the time zone values.
6224
 *		May be a Unix timestamp or any string that strtotime() can understand.
6225
 *		Defaults to 'now'.
6226
 * @return array An array of time zone identifiers and label text.
6227
 */
6228
function smf_list_timezones($when = 'now')
6229
{
6230
	global $modSettings, $tztxt, $txt, $context, $cur_profile, $sourcedir;
6231
	static $timezones_when = array();
6232
6233
	require_once($sourcedir . '/Subs-Timezones.php');
6234
6235
	// Parseable datetime string?
6236
	if (is_int($timestamp = strtotime($when)))
0 ignored issues
show
introduced by
The condition is_int($timestamp = strtotime($when)) is always true.
Loading history...
6237
		$when = $timestamp;
6238
6239
	// A Unix timestamp?
6240
	elseif (is_numeric($when))
6241
		$when = intval($when);
6242
6243
	// Invalid value? Just get current Unix timestamp.
6244
	else
6245
		$when = time();
6246
6247
	// No point doing this over if we already did it once
6248
	if (isset($timezones_when[$when]))
6249
		return $timezones_when[$when];
6250
6251
	// We'll need these too
6252
	$date_when = date_create('@' . $when);
6253
	$later = strtotime('@' . $when . ' + 1 year');
6254
6255
	// Load up any custom time zone descriptions we might have
6256
	loadLanguage('Timezones');
6257
6258
	$tzid_metazones = get_tzid_metazones($later);
6259
6260
	// Should we put time zones from certain countries at the top of the list?
6261
	$priority_countries = !empty($modSettings['timezone_priority_countries']) ? explode(',', $modSettings['timezone_priority_countries']) : array();
6262
6263
	$priority_tzids = array();
6264
	foreach ($priority_countries as $country)
6265
	{
6266
		$country_tzids = get_sorted_tzids_for_country($country);
6267
6268
		if (!empty($country_tzids))
6269
			$priority_tzids = array_merge($priority_tzids, $country_tzids);
6270
	}
6271
6272
	// Antarctic research stations should be listed last, unless you're running a penguin forum
6273
	$low_priority_tzids = !in_array('AQ', $priority_countries) ? timezone_identifiers_list(DateTimeZone::ANTARCTICA) : array();
0 ignored issues
show
Bug introduced by
Are you sure the usage of timezone_identifiers_lis...teTimeZone::ANTARCTICA) is correct as it seems to always return null.

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

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

}

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

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

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

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

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

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

}

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

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

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

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

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

6275
	$normal_priority_tzids = array_diff(array_unique(array_merge(array_keys($tzid_metazones), /** @scrutinizer ignore-type */ timezone_identifiers_list())), $priority_tzids, $low_priority_tzids);
Loading history...
6276
6277
	// Process them in order of importance.
6278
	$tzids = array_merge($priority_tzids, $normal_priority_tzids, $low_priority_tzids);
6279
6280
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
6281
	$dst_types = array();
6282
	$labels = array();
6283
	$offsets = array();
6284
	foreach ($tzids as $tzid)
6285
	{
6286
		// We don't want UTC right now
6287
		if ($tzid == 'UTC')
6288
			continue;
6289
6290
		$tz = @timezone_open($tzid);
6291
6292
		if ($tz == null)
6293
			continue;
6294
6295
		// First, get the set of transition rules for this tzid
6296
		$tzinfo = timezone_transitions_get($tz, $when, $later);
6297
6298
		// Use the entire set of transition rules as the array *key* so we can avoid duplicates
6299
		$tzkey = serialize($tzinfo);
6300
6301
		// ...But make sure to include all explicitly defined meta-zones.
6302
		if (isset($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid]))
6303
			$tzkey = serialize(array_merge($tzinfo, array('metazone' => $tzid_metazones[$tzid])));
6304
6305
		// Don't overwrite our preferred tzids
6306
		if (empty($zones[$tzkey]['tzid']))
6307
		{
6308
			$zones[$tzkey]['tzid'] = $tzid;
6309
			$zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
6310
6311
			foreach ($tzinfo as $transition) {
6312
				$zones[$tzkey]['abbrs'][] = $transition['abbr'];
6313
			}
6314
6315
			if (isset($tzid_metazones[$tzid]))
6316
				$zones[$tzkey]['metazone'] = $tzid_metazones[$tzid];
6317
			else
6318
			{
6319
				$tzgeo = timezone_location_get($tz);
6320
				$country_tzids = get_sorted_tzids_for_country($tzgeo['country_code']);
6321
6322
				if (count($country_tzids) === 1)
6323
					$zones[$tzkey]['metazone'] = $txt['iso3166'][$tzgeo['country_code']];
6324
			}
6325
		}
6326
6327
		// A time zone from a prioritized country?
6328
		if (in_array($tzid, $priority_tzids))
6329
			$priority_zones[$tzkey] = true;
6330
6331
		// Keep track of the location for this tzid.
6332
		if (!empty($txt[$tzid]))
6333
			$zones[$tzkey]['locations'][] = $txt[$tzid];
6334
		else
6335
		{
6336
			$tzid_parts = explode('/', $tzid);
6337
			$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
6338
		}
6339
6340
		// Keep track of the current offset for this tzid.
6341
		$offsets[$tzkey] = $tzinfo[0]['offset'];
6342
6343
		// Keep track of the Standard Time offset for this tzid.
6344
		foreach ($tzinfo as $transition)
6345
		{
6346
			if (!$transition['isdst'])
6347
			{
6348
				$std_offsets[$tzkey] = $transition['offset'];
6349
				break;
6350
			}
6351
		}
6352
		if (!isset($std_offsets[$tzkey]))
6353
			$std_offsets[$tzkey] = $tzinfo[0]['offset'];
6354
6355
		// Figure out the "meta-zone" info for the label
6356
		if (empty($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid]))
6357
		{
6358
			$zones[$tzkey]['metazone'] = $tzid_metazones[$tzid];
6359
			$zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
6360
		}
6361
		$dst_types[$tzkey] = count($tzinfo) > 1 ? 'c' : ($tzinfo[0]['isdst'] ? 't' : 'f');
6362
		$labels[$tzkey] = !empty($zones[$tzkey]['metazone']) && !empty($tztxt[$zones[$tzkey]['metazone']]) ? $tztxt[$zones[$tzkey]['metazone']] : '';
6363
6364
		// Remember this for later
6365
		if (isset($cur_profile['timezone']) && $cur_profile['timezone'] == $tzid)
6366
			$member_tzkey = $tzkey;
6367
		if (isset($context['event']['tz']) && $context['event']['tz'] == $tzid)
6368
			$event_tzkey = $tzkey;
6369
	}
6370
6371
	// Sort by current offset, then standard offset, then DST type, then label.
6372
	array_multisort($offsets, SORT_DESC, SORT_NUMERIC, $std_offsets, SORT_DESC, SORT_NUMERIC, $dst_types, SORT_ASC, $labels, SORT_ASC, $zones);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $zones does not seem to be defined for all execution paths leading up to this point.
Loading history...
Bug introduced by
SORT_ASC cannot be passed to array_multisort() as the parameter $rest expects a reference. ( Ignorable by Annotation )

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

6372
	array_multisort($offsets, SORT_DESC, SORT_NUMERIC, $std_offsets, SORT_DESC, SORT_NUMERIC, $dst_types, /** @scrutinizer ignore-type */ SORT_ASC, $labels, SORT_ASC, $zones);
Loading history...
Bug introduced by
SORT_NUMERIC cannot be passed to array_multisort() as the parameter $rest expects a reference. ( Ignorable by Annotation )

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

6372
	array_multisort($offsets, SORT_DESC, /** @scrutinizer ignore-type */ SORT_NUMERIC, $std_offsets, SORT_DESC, SORT_NUMERIC, $dst_types, SORT_ASC, $labels, SORT_ASC, $zones);
Loading history...
Bug introduced by
SORT_DESC cannot be passed to array_multisort() as the parameter $rest expects a reference. ( Ignorable by Annotation )

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

6372
	array_multisort($offsets, /** @scrutinizer ignore-type */ SORT_DESC, SORT_NUMERIC, $std_offsets, SORT_DESC, SORT_NUMERIC, $dst_types, SORT_ASC, $labels, SORT_ASC, $zones);
Loading history...
Comprehensibility Best Practice introduced by
The variable $std_offsets does not seem to be defined for all execution paths leading up to this point.
Loading history...
6373
6374
	// Build the final array of formatted values
6375
	$priority_timezones = array();
6376
	$timezones = array();
6377
	foreach ($zones as $tzkey => $tzvalue)
6378
	{
6379
		date_timezone_set($date_when, timezone_open($tzvalue['tzid']));
6380
6381
		// Use the human friendly time zone name, if there is one.
6382
		$desc = '';
6383
		if (!empty($tzvalue['metazone']))
6384
		{
6385
			if (!empty($tztxt[$tzvalue['metazone']]))
6386
				$metazone = $tztxt[$tzvalue['metazone']];
6387
			else
6388
				$metazone = sprintf($tztxt['generic_timezone'], $tzvalue['metazone'], '%1$s');
6389
6390
			switch ($tzvalue['dst_type'])
6391
			{
6392
				case 0:
6393
					$desc = sprintf($metazone, $tztxt['daylight_saving_time_false']);
6394
					break;
6395
6396
				case 1:
6397
					$desc = sprintf($metazone, '');
6398
					break;
6399
6400
				case 2:
6401
					$desc = sprintf($metazone, $tztxt['daylight_saving_time_true']);
6402
					break;
6403
			}
6404
		}
6405
		// Otherwise, use the list of locations (max 5, so things don't get silly)
6406
		else
6407
			$desc = implode(', ', array_slice(array_unique($tzvalue['locations']), 0, 5)) . (count($tzvalue['locations']) > 5 ? ', ' . $txt['etc'] : '');
6408
6409
		// We don't want abbreviations like '+03' or '-11'.
6410
		$abbrs = array_filter(
6411
			$tzvalue['abbrs'],
6412
			function ($abbr)
6413
			{
6414
				return !strspn($abbr, '+-');
6415
			}
6416
		);
6417
		$abbrs = count($abbrs) == count($tzvalue['abbrs']) ? array_unique($abbrs) : array();
6418
6419
		// Show the UTC offset and abbreviation(s).
6420
		$desc = '[UTC' . date_format($date_when, 'P') . '] - ' . str_replace('  ', ' ', $desc) . (!empty($abbrs) ? ' (' . implode('/', $abbrs) . ')' : '');
6421
6422
		if (isset($priority_zones[$tzkey]))
6423
			$priority_timezones[$tzvalue['tzid']] = $desc;
6424
		else
6425
			$timezones[$tzvalue['tzid']] = $desc;
6426
6427
		// Automatically fix orphaned time zones.
6428
		if (isset($member_tzkey) && $member_tzkey == $tzkey)
6429
			$cur_profile['timezone'] = $tzvalue['tzid'];
6430
		if (isset($event_tzkey) && $event_tzkey == $tzkey)
6431
			$context['event']['tz'] = $tzvalue['tzid'];
6432
	}
6433
6434
	if (!empty($priority_timezones))
6435
		$priority_timezones[] = '-----';
6436
6437
	$timezones = array_merge(
6438
		$priority_timezones,
6439
		array('UTC' => 'UTC' . (!empty($tztxt['UTC']) ? ' - ' . $tztxt['UTC'] : ''), '-----'),
6440
		$timezones
6441
	);
6442
6443
	$timezones_when[$when] = $timezones;
6444
6445
	return $timezones_when[$when];
6446
}
6447
6448
/**
6449
 * Gets a member's selected time zone identifier
6450
 *
6451
 * @param int $id_member The member id to look up. If not provided, the current user's id will be used.
6452
 * @return string The time zone identifier string for the user's time zone.
6453
 */
6454
function getUserTimezone($id_member = null)
6455
{
6456
	global $smcFunc, $context, $user_info, $modSettings, $user_settings;
6457
	static $member_cache = array();
6458
6459
	if (is_null($id_member) && $user_info['is_guest'] == false)
6460
		$id_member = $context['user']['id'];
6461
6462
	// Did we already look this up?
6463
	if (isset($id_member) && isset($member_cache[$id_member]))
6464
	{
6465
		return $member_cache[$id_member];
6466
	}
6467
6468
	// Check if we already have this in $user_settings.
6469
	if (isset($user_settings['id_member']) && $user_settings['id_member'] == $id_member && !empty($user_settings['timezone']))
6470
	{
6471
		$member_cache[$id_member] = $user_settings['timezone'];
6472
		return $user_settings['timezone'];
6473
	}
6474
6475
	// Look it up in the database.
6476
	if (isset($id_member))
6477
	{
6478
		$request = $smcFunc['db_query']('', '
6479
			SELECT timezone
6480
			FROM {db_prefix}members
6481
			WHERE id_member = {int:id_member}',
6482
			array(
6483
				'id_member' => $id_member,
6484
			)
6485
		);
6486
		list($timezone) = $smcFunc['db_fetch_row']($request);
6487
		$smcFunc['db_free_result']($request);
6488
	}
6489
6490
	// If it is invalid, fall back to the default.
6491
	if (empty($timezone) || !in_array($timezone, timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)))
0 ignored issues
show
Bug introduced by
timezone_identifiers_lis...eTimeZone::ALL_WITH_BC) of type void is incompatible with the type array expected by parameter $haystack of in_array(). ( Ignorable by Annotation )

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

6491
	if (empty($timezone) || !in_array($timezone, /** @scrutinizer ignore-type */ timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)))
Loading history...
Bug introduced by
Are you sure the usage of timezone_identifiers_lis...eTimeZone::ALL_WITH_BC) is correct as it seems to always return null.

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

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

}

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

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

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

Loading history...
6492
		$timezone = isset($modSettings['default_timezone']) ? $modSettings['default_timezone'] : date_default_timezone_get();
6493
6494
	// Save for later.
6495
	if (isset($id_member))
6496
		$member_cache[$id_member] = $timezone;
6497
6498
	return $timezone;
6499
}
6500
6501
/**
6502
 * Converts an IP address into binary
6503
 *
6504
 * @param string $ip_address An IP address in IPv4, IPv6 or decimal notation
6505
 * @return string|false The IP address in binary or false
6506
 */
6507
function inet_ptod($ip_address)
6508
{
6509
	if (!isValidIP($ip_address))
6510
		return $ip_address;
6511
6512
	$bin = inet_pton($ip_address);
6513
	return $bin;
6514
}
6515
6516
/**
6517
 * Converts a binary version of an IP address into a readable format
6518
 *
6519
 * @param string $bin An IP address in IPv4, IPv6 (Either string (postgresql) or binary (other databases))
6520
 * @return string|false The IP address in presentation format or false on error
6521
 */
6522
function inet_dtop($bin)
6523
{
6524
	global $db_type;
6525
6526
	if (empty($bin))
6527
		return '';
6528
	elseif ($db_type == 'postgresql')
6529
		return $bin;
6530
	// Already a String?
6531
	elseif (isValidIP($bin))
6532
		return $bin;
6533
	return inet_ntop($bin);
6534
}
6535
6536
/**
6537
 * Safe serialize() and unserialize() replacements
6538
 *
6539
 * @license Public Domain
6540
 *
6541
 * @author anthon (dot) pang (at) gmail (dot) com
6542
 */
6543
6544
/**
6545
 * Safe serialize() replacement. Recursive
6546
 * - output a strict subset of PHP's native serialized representation
6547
 * - does not serialize objects
6548
 *
6549
 * @param mixed $value
6550
 * @return string
6551
 */
6552
function _safe_serialize($value)
6553
{
6554
	if (is_null($value))
6555
		return 'N;';
6556
6557
	if (is_bool($value))
6558
		return 'b:' . (int) $value . ';';
6559
6560
	if (is_int($value))
6561
		return 'i:' . $value . ';';
6562
6563
	if (is_float($value))
6564
		return 'd:' . str_replace(',', '.', $value) . ';';
6565
6566
	if (is_string($value))
6567
		return 's:' . strlen($value) . ':"' . $value . '";';
6568
6569
	if (is_array($value))
6570
	{
6571
		$out = '';
6572
		foreach ($value as $k => $v)
6573
			$out .= _safe_serialize($k) . _safe_serialize($v);
6574
6575
		return 'a:' . count($value) . ':{' . $out . '}';
6576
	}
6577
6578
	// safe_serialize cannot serialize resources or objects.
6579
	return false;
6580
}
6581
6582
/**
6583
 * Wrapper for _safe_serialize() that handles exceptions and multibyte encoding issues.
6584
 *
6585
 * @param mixed $value
6586
 * @return string
6587
 */
6588
function safe_serialize($value)
6589
{
6590
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
6591
	if (function_exists('mb_internal_encoding') &&
6592
		(((int) ini_get('mbstring.func_overload')) & 2))
6593
	{
6594
		$mbIntEnc = mb_internal_encoding();
6595
		mb_internal_encoding('ASCII');
6596
	}
6597
6598
	$out = _safe_serialize($value);
6599
6600
	if (isset($mbIntEnc))
6601
		mb_internal_encoding($mbIntEnc);
6602
6603
	return $out;
6604
}
6605
6606
/**
6607
 * Safe unserialize() replacement
6608
 * - accepts a strict subset of PHP's native serialized representation
6609
 * - does not unserialize objects
6610
 *
6611
 * @param string $str
6612
 * @return mixed
6613
 * @throw Exception if $str is malformed or contains unsupported types (e.g., resources, objects)
6614
 */
6615
function _safe_unserialize($str)
6616
{
6617
	// Input  is not a string.
6618
	if (empty($str) || !is_string($str))
6619
		return false;
6620
6621
	$stack = array();
6622
	$expected = array();
6623
6624
	/*
6625
	 * states:
6626
	 *   0 - initial state, expecting a single value or array
6627
	 *   1 - terminal state
6628
	 *   2 - in array, expecting end of array or a key
6629
	 *   3 - in array, expecting value or another array
6630
	 */
6631
	$state = 0;
6632
	while ($state != 1)
6633
	{
6634
		$type = isset($str[0]) ? $str[0] : '';
6635
		if ($type == '}')
6636
			$str = substr($str, 1);
6637
6638
		elseif ($type == 'N' && $str[1] == ';')
6639
		{
6640
			$value = null;
6641
			$str = substr($str, 2);
6642
		}
6643
		elseif ($type == 'b' && preg_match('/^b:([01]);/', $str, $matches))
6644
		{
6645
			$value = $matches[1] == '1' ? true : false;
6646
			$str = substr($str, 4);
6647
		}
6648
		elseif ($type == 'i' && preg_match('/^i:(-?[0-9]+);(.*)/s', $str, $matches))
6649
		{
6650
			$value = (int) $matches[1];
6651
			$str = $matches[2];
6652
		}
6653
		elseif ($type == 'd' && preg_match('/^d:(-?[0-9]+\.?[0-9]*(E[+-][0-9]+)?);(.*)/s', $str, $matches))
6654
		{
6655
			$value = (float) $matches[1];
6656
			$str = $matches[3];
6657
		}
6658
		elseif ($type == 's' && preg_match('/^s:([0-9]+):"(.*)/s', $str, $matches) && substr($matches[2], (int) $matches[1], 2) == '";')
6659
		{
6660
			$value = substr($matches[2], 0, (int) $matches[1]);
6661
			$str = substr($matches[2], (int) $matches[1] + 2);
6662
		}
6663
		elseif ($type == 'a' && preg_match('/^a:([0-9]+):{(.*)/s', $str, $matches))
6664
		{
6665
			$expectedLength = (int) $matches[1];
6666
			$str = $matches[2];
6667
		}
6668
6669
		// Object or unknown/malformed type.
6670
		else
6671
			return false;
6672
6673
		switch ($state)
6674
		{
6675
			case 3: // In array, expecting value or another array.
6676
				if ($type == 'a')
6677
				{
6678
					$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...
6679
					$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...
6680
					$list = &$list[$key];
6681
					$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...
6682
					$state = 2;
6683
					break;
6684
				}
6685
				if ($type != '}')
6686
				{
6687
					$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...
6688
					$state = 2;
6689
					break;
6690
				}
6691
6692
				// Missing array value.
6693
				return false;
6694
6695
			case 2: // in array, expecting end of array or a key
6696
				if ($type == '}')
6697
				{
6698
					// Array size is less than expected.
6699
					if (count($list) < end($expected))
6700
						return false;
6701
6702
					unset($list);
6703
					$list = &$stack[count($stack) - 1];
6704
					array_pop($stack);
6705
6706
					// Go to terminal state if we're at the end of the root array.
6707
					array_pop($expected);
6708
6709
					if (count($expected) == 0)
6710
						$state = 1;
6711
6712
					break;
6713
				}
6714
6715
				if ($type == 'i' || $type == 's')
6716
				{
6717
					// Array size exceeds expected length.
6718
					if (count($list) >= end($expected))
6719
						return false;
6720
6721
					$key = $value;
6722
					$state = 3;
6723
					break;
6724
				}
6725
6726
				// Illegal array index type.
6727
				return false;
6728
6729
			// Expecting array or value.
6730
			case 0:
6731
				if ($type == 'a')
6732
				{
6733
					$data = array();
6734
					$list = &$data;
6735
					$expected[] = $expectedLength;
6736
					$state = 2;
6737
					break;
6738
				}
6739
6740
				if ($type != '}')
6741
				{
6742
					$data = $value;
6743
					$state = 1;
6744
					break;
6745
				}
6746
6747
				// Not in array.
6748
				return false;
6749
		}
6750
	}
6751
6752
	// Trailing data in input.
6753
	if (!empty($str))
6754
		return false;
6755
6756
	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...
6757
}
6758
6759
/**
6760
 * Wrapper for _safe_unserialize() that handles exceptions and multibyte encoding issue
6761
 *
6762
 * @param string $str
6763
 * @return mixed
6764
 */
6765
function safe_unserialize($str)
6766
{
6767
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
6768
	if (function_exists('mb_internal_encoding') &&
6769
		(((int) ini_get('mbstring.func_overload')) & 0x02))
6770
	{
6771
		$mbIntEnc = mb_internal_encoding();
6772
		mb_internal_encoding('ASCII');
6773
	}
6774
6775
	$out = _safe_unserialize($str);
6776
6777
	if (isset($mbIntEnc))
6778
		mb_internal_encoding($mbIntEnc);
6779
6780
	return $out;
6781
}
6782
6783
/**
6784
 * Tries different modes to make file/dirs writable. Wrapper function for chmod()
6785
 *
6786
 * @param string $file The file/dir full path.
6787
 * @param int $value Not needed, added for legacy reasons.
6788
 * @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.
6789
 */
6790
function smf_chmod($file, $value = 0)
6791
{
6792
	// No file? no checks!
6793
	if (empty($file))
6794
		return false;
6795
6796
	// Already writable?
6797
	if (is_writable($file))
6798
		return true;
6799
6800
	// Do we have a file or a dir?
6801
	$isDir = is_dir($file);
6802
	$isWritable = false;
6803
6804
	// Set different modes.
6805
	$chmodValues = $isDir ? array(0750, 0755, 0775, 0777) : array(0644, 0664, 0666);
6806
6807
	foreach ($chmodValues as $val)
6808
	{
6809
		// If it's writable, break out of the loop.
6810
		if (is_writable($file))
6811
		{
6812
			$isWritable = true;
6813
			break;
6814
		}
6815
6816
		else
6817
			@chmod($file, $val);
6818
	}
6819
6820
	return $isWritable;
6821
}
6822
6823
/**
6824
 * Wrapper function for json_decode() with error handling.
6825
 *
6826
 * @param string $json The string to decode.
6827
 * @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.
6828
 * @param bool $logIt To specify if the error will be logged if theres any.
6829
 * @return array Either an empty array or the decoded data as an array.
6830
 */
6831
function smf_json_decode($json, $returnAsArray = false, $logIt = true)
6832
{
6833
	global $txt;
6834
6835
	// Come on...
6836
	if (empty($json) || !is_string($json))
6837
		return array();
6838
6839
	$returnArray = @json_decode($json, $returnAsArray);
6840
6841
	// PHP 5.3 so no json_last_error_msg()
6842
	switch (json_last_error())
6843
	{
6844
		case JSON_ERROR_NONE:
6845
			$jsonError = false;
6846
			break;
6847
		case JSON_ERROR_DEPTH:
6848
			$jsonError = 'JSON_ERROR_DEPTH';
6849
			break;
6850
		case JSON_ERROR_STATE_MISMATCH:
6851
			$jsonError = 'JSON_ERROR_STATE_MISMATCH';
6852
			break;
6853
		case JSON_ERROR_CTRL_CHAR:
6854
			$jsonError = 'JSON_ERROR_CTRL_CHAR';
6855
			break;
6856
		case JSON_ERROR_SYNTAX:
6857
			$jsonError = 'JSON_ERROR_SYNTAX';
6858
			break;
6859
		case JSON_ERROR_UTF8:
6860
			$jsonError = 'JSON_ERROR_UTF8';
6861
			break;
6862
		default:
6863
			$jsonError = 'unknown';
6864
			break;
6865
	}
6866
6867
	// Something went wrong!
6868
	if (!empty($jsonError) && $logIt)
6869
	{
6870
		// Being a wrapper means we lost our smf_error_handler() privileges :(
6871
		$jsonDebug = debug_backtrace();
6872
		$jsonDebug = $jsonDebug[0];
6873
		loadLanguage('Errors');
6874
6875
		if (!empty($jsonDebug))
6876
			log_error($txt['json_' . $jsonError], 'critical', $jsonDebug['file'], $jsonDebug['line']);
6877
6878
		else
6879
			log_error($txt['json_' . $jsonError], 'critical');
6880
6881
		// Everyone expects an array.
6882
		return array();
6883
	}
6884
6885
	return $returnArray;
6886
}
6887
6888
/**
6889
 * Check the given String if he is a valid IPv4 or IPv6
6890
 * return true or false
6891
 *
6892
 * @param string $IPString
6893
 *
6894
 * @return bool
6895
 */
6896
function isValidIP($IPString)
6897
{
6898
	return filter_var($IPString, FILTER_VALIDATE_IP) !== false;
6899
}
6900
6901
/**
6902
 * Outputs a response.
6903
 * It assumes the data is already a string.
6904
 *
6905
 * @param string $data The data to print
6906
 * @param string $type The content type. Defaults to Json.
6907
 * @return void
6908
 */
6909
function smf_serverResponse($data = '', $type = 'content-type: application/json')
6910
{
6911
	global $db_show_debug, $modSettings;
6912
6913
	// Defensive programming anyone?
6914
	if (empty($data))
6915
		return false;
6916
6917
	// Don't need extra stuff...
6918
	$db_show_debug = false;
6919
6920
	// Kill anything else.
6921
	ob_end_clean();
6922
6923
	if (!empty($modSettings['CompressedOutput']))
6924
		@ob_start('ob_gzhandler');
6925
6926
	else
6927
		ob_start();
6928
6929
	// Set the header.
6930
	header($type);
6931
6932
	// Echo!
6933
	echo $data;
6934
6935
	// Done.
6936
	obExit(false);
6937
}
6938
6939
/**
6940
 * Creates an optimized regex to match all known top level domains.
6941
 *
6942
 * The optimized regex is stored in $modSettings['tld_regex'].
6943
 *
6944
 * To update the stored version of the regex to use the latest list of valid
6945
 * TLDs from iana.org, set the $update parameter to true. Updating can take some
6946
 * time, based on network connectivity, so it should normally only be done by
6947
 * calling this function from a background or scheduled task.
6948
 *
6949
 * If $update is not true, but the regex is missing or invalid, the regex will
6950
 * be regenerated from a hard-coded list of TLDs. This regenerated regex will be
6951
 * overwritten on the next scheduled update.
6952
 *
6953
 * @param bool $update If true, fetch and process the latest official list of TLDs from iana.org.
6954
 */
6955
function set_tld_regex($update = false)
6956
{
6957
	global $sourcedir, $smcFunc, $modSettings;
6958
	static $done = false;
6959
6960
	// If we don't need to do anything, don't
6961
	if (!$update && $done)
6962
		return;
6963
6964
	// Should we get a new copy of the official list of TLDs?
6965
	if ($update)
6966
	{
6967
		$tlds = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt');
6968
		$tlds_md5 = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt.md5');
6969
6970
		/**
6971
		 * If the Internet Assigned Numbers Authority can't be reached, the Internet is GONE!
6972
		 * We're probably running on a server hidden in a bunker deep underground to protect
6973
		 * it from marauding bandits roaming on the surface. We don't want to waste precious
6974
		 * electricity on pointlessly repeating background tasks, so we'll wait until the next
6975
		 * regularly scheduled update to see if civilization has been restored.
6976
		 */
6977
		if ($tlds === false || $tlds_md5 === false)
6978
			$postapocalypticNightmare = true;
6979
6980
		// Make sure nothing went horribly wrong along the way.
6981
		if (md5($tlds) != substr($tlds_md5, 0, 32))
0 ignored issues
show
Bug introduced by
It seems like $tlds can also be of type false; however, parameter $string of md5() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

6985
	elseif (!empty($modSettings['tld_regex']) && @preg_match('~' . $modSettings['tld_regex'] . '~', /** @scrutinizer ignore-type */ null) !== false)
Loading history...
6986
	{
6987
		$done = true;
6988
		return;
6989
	}
6990
6991
	// If we successfully got an update, process the list into an array
6992
	if (!empty($tlds))
6993
	{
6994
		// Clean $tlds and convert it to an array
6995
		$tlds = array_filter(
6996
			explode("\n", strtolower($tlds)),
6997
			function($line)
6998
			{
6999
				$line = trim($line);
7000
				if (empty($line) || strlen($line) != strspn($line, 'abcdefghijklmnopqrstuvwxyz0123456789-'))
7001
					return false;
7002
				else
7003
					return true;
7004
			}
7005
		);
7006
7007
		// Convert Punycode to Unicode
7008
		require_once($sourcedir . '/Class-Punycode.php');
7009
		$Punycode = new Punycode();
7010
		$tlds = array_map(
7011
			function($input) use ($Punycode)
7012
			{
7013
				return $Punycode->decode($input);
7014
			},
7015
			$tlds
7016
		);
7017
	}
7018
	// Otherwise, use the 2012 list of gTLDs and ccTLDs for now and schedule a background update
7019
	else
7020
	{
7021
		$tlds = array('com', 'net', 'org', 'edu', 'gov', 'mil', 'aero', 'asia', 'biz',
7022
			'cat', 'coop', 'info', 'int', 'jobs', 'mobi', 'museum', 'name', 'post',
7023
			'pro', 'tel', 'travel', 'xxx', 'ac', 'ad', 'ae', 'af', 'ag', 'ai', 'al',
7024
			'am', 'ao', 'aq', 'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd',
7025
			'be', 'bf', 'bg', 'bh', 'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv',
7026
			'bw', 'by', 'bz', 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm',
7027
			'cn', 'co', 'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do',
7028
			'dz', 'ec', 'ee', 'eg', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo',
7029
			'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp',
7030
			'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu',
7031
			'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm', 'jo',
7032
			'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la',
7033
			'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md',
7034
			'me', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp', 'mq', 'mr', 'ms', 'mt',
7035
			'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf', 'ng', 'ni', 'nl',
7036
			'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl',
7037
			'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw',
7038
			'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn',
7039
			'so', 'sr', 'ss', 'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg',
7040
			'th', 'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua',
7041
			'ug', 'uk', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf',
7042
			'ws', 'ye', 'yt', 'za', 'zm', 'zw',
7043
		);
7044
7045
		// Schedule a background update, unless civilization has collapsed and/or we are having connectivity issues.
7046
		if (empty($postapocalypticNightmare))
7047
		{
7048
			$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
7049
				array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
7050
				array('$sourcedir/tasks/UpdateTldRegex.php', 'Update_TLD_Regex', '', 0), array()
7051
			);
7052
		}
7053
	}
7054
7055
	// Tack on some "special use domain names" that aren't in DNS but may possibly resolve.
7056
	// See https://www.iana.org/assignments/special-use-domain-names/ for more info.
7057
	$tlds = array_merge($tlds, array('local', 'onion', 'test'));
7058
7059
	// Get an optimized regex to match all the TLDs
7060
	$tld_regex = build_regex($tlds);
7061
7062
	// Remember the new regex in $modSettings
7063
	updateSettings(array('tld_regex' => $tld_regex));
7064
7065
	// Redundant repetition is redundant
7066
	$done = true;
7067
}
7068
7069
/**
7070
 * Creates optimized regular expressions from an array of strings.
7071
 *
7072
 * An optimized regex built using this function will be much faster than a
7073
 * simple regex built using `implode('|', $strings)` --- anywhere from several
7074
 * times to several orders of magnitude faster.
7075
 *
7076
 * However, the time required to build the optimized regex is approximately
7077
 * equal to the time it takes to execute the simple regex. Therefore, it is only
7078
 * worth calling this function if the resulting regex will be used more than
7079
 * once.
7080
 *
7081
 * Because PHP places an upper limit on the allowed length of a regex, very
7082
 * large arrays of $strings may not fit in a single regex. Normally, the excess
7083
 * strings will simply be dropped. However, if the $returnArray parameter is set
7084
 * to true, this function will build as many regexes as necessary to accommodate
7085
 * everything in $strings and return them in an array. You will need to iterate
7086
 * through all elements of the returned array in order to test all possible
7087
 * matches.
7088
 *
7089
 * @param array $strings An array of strings to make a regex for.
7090
 * @param string $delim An optional delimiter character to pass to preg_quote().
7091
 * @param bool $returnArray If true, returns an array of regexes.
7092
 * @return string|array One or more regular expressions to match any of the input strings.
7093
 */
7094
function build_regex($strings, $delim = null, $returnArray = false)
7095
{
7096
	global $smcFunc;
7097
	static $regexes = array();
7098
7099
	// If it's not an array, there's not much to do. ;)
7100
	if (!is_array($strings))
0 ignored issues
show
introduced by
The condition is_array($strings) is always true.
Loading history...
7101
		return preg_quote(@strval($strings), $delim);
7102
7103
	$regex_key = md5(json_encode(array($strings, $delim, $returnArray)));
7104
7105
	if (isset($regexes[$regex_key]))
7106
		return $regexes[$regex_key];
7107
7108
	// The mb_* functions are faster than the $smcFunc ones, but may not be available
7109
	if (function_exists('mb_internal_encoding') && function_exists('mb_detect_encoding') && function_exists('mb_strlen') && function_exists('mb_substr'))
7110
	{
7111
		if (($string_encoding = mb_detect_encoding(implode(' ', $strings))) !== false)
7112
		{
7113
			$current_encoding = mb_internal_encoding();
7114
			mb_internal_encoding($string_encoding);
7115
		}
7116
7117
		$strlen = 'mb_strlen';
7118
		$substr = 'mb_substr';
7119
	}
7120
	else
7121
	{
7122
		$strlen = $smcFunc['strlen'];
7123
		$substr = $smcFunc['substr'];
7124
	}
7125
7126
	// This recursive function creates the index array from the strings
7127
	$add_string_to_index = function($string, $index) use (&$strlen, &$substr, &$add_string_to_index)
7128
	{
7129
		static $depth = 0;
7130
		$depth++;
7131
7132
		$first = (string) @$substr($string, 0, 1);
7133
7134
		// No first character? That's no good.
7135
		if ($first === '')
7136
		{
7137
			// A nested array? Really? Ugh. Fine.
7138
			if (is_array($string) && $depth < 20)
7139
			{
7140
				foreach ($string as $str)
7141
					$index = $add_string_to_index($str, $index);
7142
			}
7143
7144
			$depth--;
7145
			return $index;
7146
		}
7147
7148
		if (empty($index[$first]))
7149
			$index[$first] = array();
7150
7151
		if ($strlen($string) > 1)
7152
		{
7153
			// Sanity check on recursion
7154
			if ($depth > 99)
7155
				$index[$first][$substr($string, 1)] = '';
7156
7157
			else
7158
				$index[$first] = $add_string_to_index($substr($string, 1), $index[$first]);
7159
		}
7160
		else
7161
			$index[$first][''] = '';
7162
7163
		$depth--;
7164
		return $index;
7165
	};
7166
7167
	// This recursive function turns the index array into a regular expression
7168
	$index_to_regex = function(&$index, $delim) use (&$strlen, &$index_to_regex)
7169
	{
7170
		static $depth = 0;
7171
		$depth++;
7172
7173
		// Absolute max length for a regex is 32768, but we might need wiggle room
7174
		$max_length = 30000;
7175
7176
		$regex = array();
7177
		$length = 0;
7178
7179
		foreach ($index as $key => $value)
7180
		{
7181
			$key_regex = preg_quote($key, $delim);
7182
			$new_key = $key;
7183
7184
			if (empty($value))
7185
				$sub_regex = '';
7186
			else
7187
			{
7188
				$sub_regex = $index_to_regex($value, $delim);
7189
7190
				if (count(array_keys($value)) == 1)
7191
				{
7192
					$new_key_array = explode('(?' . '>', $sub_regex);
7193
					$new_key .= $new_key_array[0];
7194
				}
7195
				else
7196
					$sub_regex = '(?' . '>' . $sub_regex . ')';
7197
			}
7198
7199
			if ($depth > 1)
7200
				$regex[$new_key] = $key_regex . $sub_regex;
7201
			else
7202
			{
7203
				if (($length += strlen($key_regex . $sub_regex) + 1) < $max_length || empty($regex))
7204
				{
7205
					$regex[$new_key] = $key_regex . $sub_regex;
7206
					unset($index[$key]);
7207
				}
7208
				else
7209
					break;
7210
			}
7211
		}
7212
7213
		// Sort by key length and then alphabetically
7214
		uksort(
7215
			$regex,
7216
			function($k1, $k2) use (&$strlen)
7217
			{
7218
				$l1 = $strlen($k1);
7219
				$l2 = $strlen($k2);
7220
7221
				if ($l1 == $l2)
7222
					return strcmp($k1, $k2) > 0 ? 1 : -1;
7223
				else
7224
					return $l1 > $l2 ? -1 : 1;
7225
			}
7226
		);
7227
7228
		$depth--;
7229
		return implode('|', $regex);
7230
	};
7231
7232
	// Now that the functions are defined, let's do this thing
7233
	$index = array();
7234
	$regex = '';
7235
7236
	foreach ($strings as $string)
7237
		$index = $add_string_to_index($string, $index);
7238
7239
	if ($returnArray === true)
7240
	{
7241
		$regex = array();
7242
		while (!empty($index))
7243
			$regex[] = '(?' . '>' . $index_to_regex($index, $delim) . ')';
7244
	}
7245
	else
7246
		$regex = '(?' . '>' . $index_to_regex($index, $delim) . ')';
7247
7248
	// Restore PHP's internal character encoding to whatever it was originally
7249
	if (!empty($current_encoding))
7250
		mb_internal_encoding($current_encoding);
7251
7252
	$regexes[$regex_key] = $regex;
7253
	return $regex;
7254
}
7255
7256
/**
7257
 * Check if the passed url has an SSL certificate.
7258
 *
7259
 * Returns true if a cert was found & false if not.
7260
 *
7261
 * @param string $url to check, in $boardurl format (no trailing slash).
7262
 */
7263
function ssl_cert_found($url)
7264
{
7265
	// This check won't work without OpenSSL
7266
	if (!extension_loaded('openssl'))
7267
		return true;
7268
7269
	// First, strip the subfolder from the passed url, if any
7270
	$parsedurl = parse_url($url);
7271
	$url = 'ssl://' . $parsedurl['host'] . ':443';
7272
7273
	// Next, check the ssl stream context for certificate info
7274
	if (version_compare(PHP_VERSION, '5.6.0', '<'))
7275
		$ssloptions = array("capture_peer_cert" => true);
7276
	else
7277
		$ssloptions = array("capture_peer_cert" => true, "verify_peer" => true, "allow_self_signed" => true);
7278
7279
	$result = false;
7280
	$context = stream_context_create(array("ssl" => $ssloptions));
7281
	$stream = @stream_socket_client($url, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
7282
	if ($stream !== false)
7283
	{
7284
		$params = stream_context_get_params($stream);
7285
		$result = isset($params["options"]["ssl"]["peer_certificate"]) ? true : false;
7286
	}
7287
	return $result;
7288
}
7289
7290
/**
7291
 * Check if the passed url has a redirect to https:// by querying headers.
7292
 *
7293
 * Returns true if a redirect was found & false if not.
7294
 * Note that when force_ssl = 2, SMF issues its own redirect...  So if this
7295
 * returns true, it may be caused by SMF, not necessarily an .htaccess redirect.
7296
 *
7297
 * @param string $url to check, in $boardurl format (no trailing slash).
7298
 */
7299
function https_redirect_active($url)
7300
{
7301
	// Ask for the headers for the passed url, but via http...
7302
	// Need to add the trailing slash, or it puts it there & thinks there's a redirect when there isn't...
7303
	$url = str_ireplace('https://', 'http://', $url) . '/';
0 ignored issues
show
Bug introduced by
Are you sure str_ireplace('https://', 'http://', $url) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

7303
	$url = /** @scrutinizer ignore-type */ str_ireplace('https://', 'http://', $url) . '/';
Loading history...
7304
	$headers = @get_headers($url);
7305
	if ($headers === false)
7306
		return false;
7307
7308
	// Now to see if it came back https...
7309
	// First check for a redirect status code in first row (301, 302, 307)
7310
	if (strstr($headers[0], '301') === false && strstr($headers[0], '302') === false && strstr($headers[0], '307') === false)
7311
		return false;
7312
7313
	// Search for the location entry to confirm https
7314
	$result = false;
7315
	foreach ($headers as $header)
7316
	{
7317
		if (stristr($header, 'Location: https://') !== false)
7318
		{
7319
			$result = true;
7320
			break;
7321
		}
7322
	}
7323
	return $result;
7324
}
7325
7326
/**
7327
 * Build query_wanna_see_board and query_see_board for a userid
7328
 *
7329
 * Returns array with keys query_wanna_see_board and query_see_board
7330
 *
7331
 * @param int $userid of the user
7332
 */
7333
function build_query_board($userid)
7334
{
7335
	global $user_info, $modSettings, $smcFunc, $db_prefix;
7336
7337
	$query_part = array();
7338
7339
	// If we come from cron, we can't have a $user_info.
7340
	if (isset($user_info['id']) && $user_info['id'] == $userid && SMF != 'BACKGROUND')
7341
	{
7342
		$groups = $user_info['groups'];
7343
		$can_see_all_boards = $user_info['is_admin'] || $user_info['can_manage_boards'];
7344
		$ignoreboards = !empty($user_info['ignoreboards']) ? $user_info['ignoreboards'] : null;
7345
	}
7346
	else
7347
	{
7348
		$request = $smcFunc['db_query']('', '
7349
			SELECT mem.ignore_boards, mem.id_group, mem.additional_groups, mem.id_post_group
7350
			FROM {db_prefix}members AS mem
7351
			WHERE mem.id_member = {int:id_member}
7352
			LIMIT 1',
7353
			array(
7354
				'id_member' => $userid,
7355
			)
7356
		);
7357
7358
		$row = $smcFunc['db_fetch_assoc']($request);
7359
7360
		if (empty($row['additional_groups']))
7361
			$groups = array($row['id_group'], $row['id_post_group']);
7362
		else
7363
			$groups = array_merge(
7364
				array($row['id_group'], $row['id_post_group']),
7365
				explode(',', $row['additional_groups'])
7366
			);
7367
7368
		// Because history has proven that it is possible for groups to go bad - clean up in case.
7369
		foreach ($groups as $k => $v)
7370
			$groups[$k] = (int) $v;
7371
7372
		$can_see_all_boards = in_array(1, $groups) || (!empty($modSettings['board_manager_groups']) && count(array_intersect($groups, explode(',', $modSettings['board_manager_groups']))) > 0);
7373
7374
		$ignoreboards = !empty($row['ignore_boards']) && !empty($modSettings['allow_ignore_boards']) ? explode(',', $row['ignore_boards']) : array();
7375
	}
7376
7377
	// Just build this here, it makes it easier to change/use - administrators can see all boards.
7378
	if ($can_see_all_boards)
7379
		$query_part['query_see_board'] = '1=1';
7380
	// Otherwise just the groups in $user_info['groups'].
7381
	else
7382
	{
7383
		$query_part['query_see_board'] = '
7384
			EXISTS (
7385
				SELECT bpv.id_board
7386
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
7387
				WHERE bpv.id_group IN ('. implode(',', $groups) .')
7388
					AND bpv.deny = 0
7389
					AND bpv.id_board = b.id_board
7390
			)';
7391
7392
		if (!empty($modSettings['deny_boards_access']))
7393
			$query_part['query_see_board'] .= '
7394
			AND NOT EXISTS (
7395
				SELECT bpv.id_board
7396
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
7397
				WHERE bpv.id_group IN ( '. implode(',', $groups) .')
7398
					AND bpv.deny = 1
7399
					AND bpv.id_board = b.id_board
7400
			)';
7401
	}
7402
7403
	$query_part['query_see_message_board'] = str_replace('b.', 'm.', $query_part['query_see_board']);
7404
	$query_part['query_see_topic_board'] = str_replace('b.', 't.', $query_part['query_see_board']);
7405
7406
	// Build the list of boards they WANT to see.
7407
	// This will take the place of query_see_boards in certain spots, so it better include the boards they can see also
7408
7409
	// If they aren't ignoring any boards then they want to see all the boards they can see
7410
	if (empty($ignoreboards))
7411
	{
7412
		$query_part['query_wanna_see_board'] = $query_part['query_see_board'];
7413
		$query_part['query_wanna_see_message_board'] = $query_part['query_see_message_board'];
7414
		$query_part['query_wanna_see_topic_board'] = $query_part['query_see_topic_board'];
7415
	}
7416
	// Ok I guess they don't want to see all the boards
7417
	else
7418
	{
7419
		$query_part['query_wanna_see_board'] = '(' . $query_part['query_see_board'] . ' AND b.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7420
		$query_part['query_wanna_see_message_board'] = '(' . $query_part['query_see_message_board'] . ' AND m.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7421
		$query_part['query_wanna_see_topic_board'] = '(' . $query_part['query_see_topic_board'] . ' AND t.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7422
	}
7423
7424
	return $query_part;
7425
}
7426
7427
/**
7428
 * Check if the connection is using https.
7429
 *
7430
 * @return boolean true if connection used https
7431
 */
7432
function httpsOn()
7433
{
7434
	$secure = false;
7435
7436
	if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on')
7437
		$secure = true;
7438
	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...
7439
		$secure = true;
7440
7441
	return $secure;
7442
}
7443
7444
/**
7445
 * A wrapper for `filter_var($url, FILTER_VALIDATE_URL)` that can handle URLs
7446
 * with international characters (a.k.a. IRIs)
7447
 *
7448
 * @param string $iri The IRI to test.
7449
 * @param int $flags Optional flags to pass to filter_var()
7450
 * @return string|bool Either the original IRI, or false if the IRI was invalid.
7451
 */
7452
function validate_iri($iri, $flags = null)
7453
{
7454
	$url = iri_to_url($iri);
7455
7456
	// PHP 5 doesn't recognize IPv6 addresses in the URL host.
7457
	if (version_compare(phpversion(), '7.0.0', '<'))
7458
	{
7459
		$host = parse_url((strpos($url, '//') === 0 ? 'http:' : '') . $url, PHP_URL_HOST);
7460
7461
		if (strpos($host, '[') === 0 && strpos($host, ']') === strlen($host) - 1 && strpos($host, ':') !== false)
7462
			$url = str_replace($host, '127.0.0.1', $url);
7463
	}
7464
7465
	if (filter_var($url, FILTER_VALIDATE_URL, $flags) !== false)
0 ignored issues
show
Bug introduced by
It seems like $flags can also be of type null; however, parameter $options of filter_var() does only seem to accept array|integer, maybe add an additional type check? ( Ignorable by Annotation )

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

7465
	if (filter_var($url, FILTER_VALIDATE_URL, /** @scrutinizer ignore-type */ $flags) !== false)
Loading history...
7466
		return $iri;
7467
	else
7468
		return false;
7469
}
7470
7471
/**
7472
 * A wrapper for `filter_var($url, FILTER_SANITIZE_URL)` that can handle URLs
7473
 * with international characters (a.k.a. IRIs)
7474
 *
7475
 * Note: The returned value will still be an IRI, not a URL. To convert to URL,
7476
 * feed the result of this function to iri_to_url()
7477
 *
7478
 * @param string $iri The IRI to sanitize.
7479
 * @return string|bool The sanitized version of the IRI
7480
 */
7481
function sanitize_iri($iri)
7482
{
7483
	// Encode any non-ASCII characters (but not space or control characters of any sort)
7484
	// Also encode '%' in order to preserve anything that is already percent-encoded.
7485
	$iri = preg_replace_callback(
7486
		'~[^\x00-\x7F\pZ\pC]|%~u',
7487
		function($matches)
7488
		{
7489
			return rawurlencode($matches[0]);
7490
		},
7491
		$iri
7492
	);
7493
7494
	// Perform normal sanitization
7495
	$iri = filter_var($iri, FILTER_SANITIZE_URL);
7496
7497
	// Decode the non-ASCII characters
7498
	$iri = rawurldecode($iri);
7499
7500
	return $iri;
7501
}
7502
7503
/**
7504
 * Converts a URL with international characters (an IRI) into a pure ASCII URL
7505
 *
7506
 * Uses Punycode to encode any non-ASCII characters in the domain name, and uses
7507
 * standard URL encoding on the rest.
7508
 *
7509
 * @param string $iri A IRI that may or may not contain non-ASCII characters.
7510
 * @return string|bool The URL version of the IRI.
7511
 */
7512
function iri_to_url($iri)
7513
{
7514
	global $sourcedir;
7515
7516
	$host = parse_url((strpos($iri, '//') === 0 ? 'http:' : '') . $iri, PHP_URL_HOST);
7517
7518
	if (!empty($host))
7519
	{
7520
		// Convert the host using the Punycode algorithm
7521
		require_once($sourcedir . '/Class-Punycode.php');
7522
		$Punycode = new Punycode();
7523
		$encoded_host = $Punycode->encode($host);
7524
7525
		$pos = strpos($iri, $host);
7526
	}
7527
	else
7528
	{
7529
		$encoded_host = '';
7530
		$pos = 0;
7531
	}
7532
7533
	$before_host = substr($iri, 0, $pos);
7534
	$after_host = substr($iri, $pos + strlen($host));
7535
7536
	// Encode any disallowed characters in the rest of the URL
7537
	$unescaped = array(
7538
		'%21' => '!', '%23' => '#', '%24' => '$', '%26' => '&',
7539
		'%27' => "'", '%28' => '(', '%29' => ')', '%2A' => '*',
7540
		'%2B' => '+', '%2C' => ',', '%2F' => '/', '%3A' => ':',
7541
		'%3B' => ';', '%3D' => '=', '%3F' => '?', '%40' => '@',
7542
		'%25' => '%',
7543
	);
7544
7545
	$before_host = strtr(rawurlencode($before_host), $unescaped);
7546
	$after_host = strtr(rawurlencode($after_host), $unescaped);
7547
7548
	return $before_host . $encoded_host . $after_host;
7549
}
7550
7551
/**
7552
 * Decodes a URL containing encoded international characters to UTF-8
7553
 *
7554
 * Decodes any Punycode encoded characters in the domain name, then uses
7555
 * standard URL decoding on the rest.
7556
 *
7557
 * @param string $url The pure ASCII version of a URL.
7558
 * @return string|bool The UTF-8 version of the URL.
7559
 */
7560
function url_to_iri($url)
7561
{
7562
	global $sourcedir;
7563
7564
	$host = parse_url((strpos($url, '//') === 0 ? 'http:' : '') . $url, PHP_URL_HOST);
7565
7566
	if (!empty($host))
7567
	{
7568
		// Decode the domain from Punycode
7569
		require_once($sourcedir . '/Class-Punycode.php');
7570
		$Punycode = new Punycode();
7571
		$decoded_host = $Punycode->decode($host);
7572
7573
		$pos = strpos($url, $host);
7574
	}
7575
	else
7576
	{
7577
		$decoded_host = '';
7578
		$pos = 0;
7579
	}
7580
7581
	$before_host = substr($url, 0, $pos);
7582
	$after_host = substr($url, $pos + strlen($host));
7583
7584
	// Decode the rest of the URL
7585
	$before_host = rawurldecode($before_host);
7586
	$after_host = rawurldecode($after_host);
7587
7588
	return $before_host . $decoded_host . $after_host;
7589
}
7590
7591
/**
7592
 * Ensures SMF's scheduled tasks are being run as intended
7593
 *
7594
 * If the admin activated the cron_is_real_cron setting, but the cron job is
7595
 * not running things at least once per day, we need to go back to SMF's default
7596
 * behaviour using "web cron" JavaScript calls.
7597
 */
7598
function check_cron()
7599
{
7600
	global $modSettings, $smcFunc, $txt;
7601
7602
	if (!empty($modSettings['cron_is_real_cron']) && time() - @intval($modSettings['cron_last_checked']) > 84600)
7603
	{
7604
		$request = $smcFunc['db_query']('', '
7605
			SELECT COUNT(*)
7606
			FROM {db_prefix}scheduled_tasks
7607
			WHERE disabled = {int:not_disabled}
7608
				AND next_time < {int:yesterday}',
7609
			array(
7610
				'not_disabled' => 0,
7611
				'yesterday' => time() - 84600,
7612
			)
7613
		);
7614
		list($overdue) = $smcFunc['db_fetch_row']($request);
7615
		$smcFunc['db_free_result']($request);
7616
7617
		// If we have tasks more than a day overdue, cron isn't doing its job.
7618
		if (!empty($overdue))
7619
		{
7620
			loadLanguage('ManageScheduledTasks');
7621
			log_error($txt['cron_not_working']);
7622
			updateSettings(array('cron_is_real_cron' => 0));
7623
		}
7624
		else
7625
			updateSettings(array('cron_last_checked' => time()));
7626
	}
7627
}
7628
7629
/**
7630
 * Sends an appropriate HTTP status header based on a given status code
7631
 *
7632
 * @param int $code The status code
7633
 * @param string $status The string for the status. Set automatically if not provided.
7634
 */
7635
function send_http_status($code, $status = '')
7636
{
7637
	$statuses = array(
7638
		204 => 'No Content',
7639
		206 => 'Partial Content',
7640
		304 => 'Not Modified',
7641
		400 => 'Bad Request',
7642
		403 => 'Forbidden',
7643
		404 => 'Not Found',
7644
		410 => 'Gone',
7645
		500 => 'Internal Server Error',
7646
		503 => 'Service Unavailable',
7647
	);
7648
7649
	$protocol = preg_match('~^\s*(HTTP/[12]\.\d)\s*$~i', $_SERVER['SERVER_PROTOCOL'], $matches) ? $matches[1] : 'HTTP/1.0';
7650
7651
	// Typically during these requests, we have cleaned the response (ob_*clean), ensure these headers exist.
7652
	frameOptionsHeader();
7653
	corsPolicyHeader();
7654
7655
	if (!isset($statuses[$code]) && empty($status))
7656
		header($protocol . ' 500 Internal Server Error');
7657
	else
7658
		header($protocol . ' ' . $code . ' ' . (!empty($status) ? $status : $statuses[$code]));
7659
}
7660
7661
/**
7662
 * Concatenates an array of strings into a grammatically correct sentence list
7663
 *
7664
 * Uses formats defined in the language files to build the list appropropriately
7665
 * for the currently loaded language.
7666
 *
7667
 * @param array $list An array of strings to concatenate.
7668
 * @return string The localized sentence list.
7669
 */
7670
function sentence_list($list)
7671
{
7672
	global $txt;
7673
7674
	// Make sure the bare necessities are defined
7675
	if (empty($txt['sentence_list_format']['n']))
7676
		$txt['sentence_list_format']['n'] = '{series}';
7677
	if (!isset($txt['sentence_list_separator']))
7678
		$txt['sentence_list_separator'] = ', ';
7679
	if (!isset($txt['sentence_list_separator_alt']))
7680
		$txt['sentence_list_separator_alt'] = '; ';
7681
7682
	// Which format should we use?
7683
	if (isset($txt['sentence_list_format'][count($list)]))
7684
		$format = $txt['sentence_list_format'][count($list)];
7685
	else
7686
		$format = $txt['sentence_list_format']['n'];
7687
7688
	// Do we want the normal separator or the alternate?
7689
	$separator = $txt['sentence_list_separator'];
7690
	foreach ($list as $item)
7691
	{
7692
		if (strpos($item, $separator) !== false)
7693
		{
7694
			$separator = $txt['sentence_list_separator_alt'];
7695
			$format = strtr($format, trim($txt['sentence_list_separator']), trim($separator));
7696
			break;
7697
		}
7698
	}
7699
7700
	$replacements = array();
7701
7702
	// Special handling for the last items on the list
7703
	$i = 0;
7704
	while (empty($done))
7705
	{
7706
		if (strpos($format, '{'. --$i . '}') !== false)
7707
			$replacements['{'. $i . '}'] = array_pop($list);
7708
		else
7709
			$done = true;
7710
	}
7711
	unset($done);
7712
7713
	// Special handling for the first items on the list
7714
	$i = 0;
7715
	while (empty($done))
7716
	{
7717
		if (strpos($format, '{'. ++$i . '}') !== false)
7718
			$replacements['{'. $i . '}'] = array_shift($list);
7719
		else
7720
			$done = true;
7721
	}
7722
	unset($done);
7723
7724
	// Whatever is left
7725
	$replacements['{series}'] = implode($separator, $list);
7726
7727
	// Do the deed
7728
	return strtr($format, $replacements);
7729
}
7730
7731
/**
7732
 * Truncate an array to a specified length
7733
 *
7734
 * @param array $array The array to truncate
7735
 * @param int $max_length The upperbound on the length
7736
 * @param int $deep How levels in an multidimensional array should the function take into account.
7737
 * @return array The truncated array
7738
 */
7739
function truncate_array($array, $max_length = 1900, $deep = 3)
7740
{
7741
	$array = (array) $array;
7742
7743
	$curr_length = array_length($array, $deep);
7744
7745
	if ($curr_length <= $max_length)
7746
		return $array;
7747
7748
	else
7749
	{
7750
		// Truncate each element's value to a reasonable length
7751
		$param_max = floor($max_length / count($array));
7752
7753
		$current_deep = $deep - 1;
7754
7755
		foreach ($array as $key => &$value)
7756
		{
7757
			if (is_array($value))
7758
				if ($current_deep > 0)
7759
					$value = truncate_array($value, $current_deep);
7760
7761
			else
7762
				$value = substr($value, 0, $param_max - strlen($key) - 5);
0 ignored issues
show
Bug introduced by
$value of type array is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

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

7762
				$value = substr(/** @scrutinizer ignore-type */ $value, 0, $param_max - strlen($key) - 5);
Loading history...
Bug introduced by
$param_max - strlen($key) - 5 of type double is incompatible with the type integer|null expected by parameter $length of substr(). ( Ignorable by Annotation )

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

7762
				$value = substr($value, 0, /** @scrutinizer ignore-type */ $param_max - strlen($key) - 5);
Loading history...
7763
		}
7764
7765
		return $array;
7766
	}
7767
}
7768
7769
/**
7770
 * array_length Recursive
7771
 * @param array $array
7772
 * @param int $deep How many levels should the function
7773
 * @return int
7774
 */
7775
function array_length($array, $deep = 3)
7776
{
7777
	// Work with arrays
7778
	$array = (array) $array;
7779
	$length = 0;
7780
7781
	$deep_count = $deep - 1;
7782
7783
	foreach ($array as $value)
7784
	{
7785
		// Recursive?
7786
		if (is_array($value))
7787
		{
7788
			// No can't do
7789
			if ($deep_count <= 0)
7790
				continue;
7791
7792
			$length += array_length($value, $deep_count);
7793
		}
7794
		else
7795
			$length += strlen($value);
7796
	}
7797
7798
	return $length;
7799
}
7800
7801
/**
7802
 * Compares existance request variables against an array.
7803
 *
7804
 * The input array is associative, where keys denote accepted values
7805
 * in a request variable denoted by `$req_val`. Values can be:
7806
 *
7807
 * - another associative array where at least one key must be found
7808
 *   in the request and their values are accepted request values.
7809
 * - A scalar value, in which case no furthur checks are done.
7810
 *
7811
 * @param array $array
7812
 * @param string $req_var request variable
7813
 *
7814
 * @return bool whether any of the criteria was satisfied
7815
 */
7816
function is_filtered_request(array $array, $req_var)
7817
{
7818
	$matched = false;
7819
	if (isset($_REQUEST[$req_var], $array[$_REQUEST[$req_var]]))
7820
	{
7821
		if (is_array($array[$_REQUEST[$req_var]]))
7822
		{
7823
			foreach ($array[$_REQUEST[$req_var]] as $subtype => $subnames)
7824
				$matched |= isset($_REQUEST[$subtype]) && in_array($_REQUEST[$subtype], $subnames);
7825
		}
7826
		else
7827
			$matched = true;
7828
	}
7829
7830
	return (bool) $matched;
7831
}
7832
7833
/**
7834
 * Clean up the XML to make sure it doesn't contain invalid characters.
7835
 *
7836
 * See https://www.w3.org/TR/xml/#charsets
7837
 *
7838
 * @param string $string The string to clean
7839
 * @return string The cleaned string
7840
 */
7841
function cleanXml($string)
7842
{
7843
	global $context;
7844
7845
	$illegal_chars = array(
7846
		// Remove all ASCII control characters except \t, \n, and \r.
7847
		"\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08",
7848
		"\x0B", "\x0C", "\x0E", "\x0F", "\x10", "\x11", "\x12", "\x13", "\x14",
7849
		"\x15", "\x16", "\x17", "\x18", "\x19", "\x1A", "\x1B", "\x1C", "\x1D",
7850
		"\x1E", "\x1F",
7851
		// Remove \xFFFE and \xFFFF
7852
		"\xEF\xBF\xBE", "\xEF\xBF\xBF",
7853
	);
7854
7855
	$string = str_replace($illegal_chars, '', $string);
7856
7857
	// The Unicode surrogate pair code points should never be present in our
7858
	// strings to begin with, but if any snuck in, they need to be removed.
7859
	if (!empty($context['utf8']) && strpos($string, "\xED") !== false)
7860
		$string = preg_replace('/\xED[\xA0-\xBF][\x80-\xBF]/', '', $string);
7861
7862
	return $string;
7863
}
7864
7865
/**
7866
 * Escapes (replaces) characters in strings to make them safe for use in javascript
7867
 *
7868
 * @param string $string The string to escape
7869
 * @return string The escaped string
7870
 */
7871
function JavaScriptEscape($string)
7872
{
7873
	global $scripturl;
7874
7875
	return '\'' . strtr($string, array(
7876
		"\r" => '',
7877
		"\n" => '\\n',
7878
		"\t" => '\\t',
7879
		'\\' => '\\\\',
7880
		'\'' => '\\\'',
7881
		'</' => '<\' + \'/',
7882
		'<script' => '<scri\'+\'pt',
7883
		'<body>' => '<bo\'+\'dy>',
7884
		'<a href' => '<a hr\'+\'ef',
7885
		$scripturl => '\' + smf_scripturl + \'',
7886
	)) . '\'';
7887
}
7888
7889
/**
7890
 * Outputs a list of domains to attempt to prefetch (resolve) in the document head before the resource
7891
 * at that domain is requested in the DOM build. This can help speed up page load times.
7892
  */
7893
function template_dns_prefetch()
7894
{
7895
	global $modSettings;
7896
7897
	// ToDo: We can put dns-preconnect here as well if we want to
7898
	
7899
	// prefetch domains is a comma seperated list
7900
	$dns_prefetch_domains = explode(',', $modSettings['http_dns_prefetch_domains']);
7901
7902
	foreach ($dns_prefetch_domains as $domain)
7903
	{
7904
		// Yes, we want a newline
7905
		echo  '
7906
		<link rel="dns-prefetch" href="'.$domain.'">';
7907
	}	
7908
}
7909
7910
?>