Passed
Pull Request — release-2.1 (#6754)
by Jon
04:46
created

ip2range()   F

Complexity

Conditions 19
Paths 522

Size

Total Lines 72
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 43
c 0
b 0
f 0
nop 1
dl 0
loc 72
rs 1.0138
nc 522

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file has all the main functions in it that relate to, well, everything.
5
 *
6
 * Simple Machines Forum (SMF)
7
 *
8
 * @package SMF
9
 * @author Simple Machines 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 RC3
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
	// Make sure we are using the correct locale.
853
	if (!isset($locale) || ($process_safe === true && setlocale(LC_TIME, '0') != $locale))
854
		$locale = setlocale(LC_TIME, array($txt['lang_locale'] . '.' . $modSettings['global_character_set'], $txt['lang_locale'] . '.' . $txt['lang_character_set'], $txt['lang_locale']));
855
856
	// If the current locale is unsupported, we'll have to localize the hard way.
857
	if ($locale === false)
858
	{
859
		$timeformat = strtr($timeformat, array(
860
			'%a' => '#txt_days_short_%w#',
861
			'%A' => '#txt_days_%w#',
862
			'%b' => '#txt_months_short_%m#',
863
			'%B' => '#txt_months_%m#',
864
			'%p' => '&#37;p',
865
			'%P' => '&#37;p'
866
		));
867
	}
868
	// Just in case the locale doesn't support '%p' properly.
869
	// @todo Is this even necessary?
870
	else
871
	{
872
		if (!isset($non_twelve_hour) && strpos($timeformat, '%p') !== false)
873
			$non_twelve_hour = trim(strftime('%p')) === '';
874
875
		if (!empty($non_twelve_hour))
876
			$timeformat = strtr($timeformat, array(
877
				'%p' => '&#37;p',
878
				'%P' => '&#37;p'
879
			));
880
	}
881
882
	// And now, the moment we've all be waiting for...
883
	$timestring = strftime($timeformat, $log_time);
884
885
	// Do-it-yourself time localization.  Fun.
886
	if (strpos($timestring, '&#37;p') !== false)
887
		$timestring = str_replace('&#37;p', (strftime('%H', $log_time) < 12 ? $txt['time_am'] : $txt['time_pm']), $timestring);
888
	if (strpos($timestring, '#txt_') !== false)
889
	{
890
		if (strpos($timestring, '#txt_days_short_') !== false)
891
			$timestring = strtr($timestring, array(
892
				'#txt_days_short_0#' => $txt['days_short'][0],
893
				'#txt_days_short_1#' => $txt['days_short'][1],
894
				'#txt_days_short_2#' => $txt['days_short'][2],
895
				'#txt_days_short_3#' => $txt['days_short'][3],
896
				'#txt_days_short_4#' => $txt['days_short'][4],
897
				'#txt_days_short_5#' => $txt['days_short'][5],
898
				'#txt_days_short_6#' => $txt['days_short'][6],
899
			));
900
901
		if (strpos($timestring, '#txt_days_') !== false)
902
			$timestring = strtr($timestring, array(
903
				'#txt_days_0#' => $txt['days'][0],
904
				'#txt_days_1#' => $txt['days'][1],
905
				'#txt_days_2#' => $txt['days'][2],
906
				'#txt_days_3#' => $txt['days'][3],
907
				'#txt_days_4#' => $txt['days'][4],
908
				'#txt_days_5#' => $txt['days'][5],
909
				'#txt_days_6#' => $txt['days'][6],
910
			));
911
912
		if (strpos($timestring, '#txt_months_short_') !== false)
913
			$timestring = strtr($timestring, array(
914
				'#txt_months_short_01#' => $txt['months_short'][1],
915
				'#txt_months_short_02#' => $txt['months_short'][2],
916
				'#txt_months_short_03#' => $txt['months_short'][3],
917
				'#txt_months_short_04#' => $txt['months_short'][4],
918
				'#txt_months_short_05#' => $txt['months_short'][5],
919
				'#txt_months_short_06#' => $txt['months_short'][6],
920
				'#txt_months_short_07#' => $txt['months_short'][7],
921
				'#txt_months_short_08#' => $txt['months_short'][8],
922
				'#txt_months_short_09#' => $txt['months_short'][9],
923
				'#txt_months_short_10#' => $txt['months_short'][10],
924
				'#txt_months_short_11#' => $txt['months_short'][11],
925
				'#txt_months_short_12#' => $txt['months_short'][12],
926
			));
927
928
		if (strpos($timestring, '#txt_months_') !== false)
929
			$timestring = strtr($timestring, array(
930
				'#txt_months_01#' => $txt['months'][1],
931
				'#txt_months_02#' => $txt['months'][2],
932
				'#txt_months_03#' => $txt['months'][3],
933
				'#txt_months_04#' => $txt['months'][4],
934
				'#txt_months_05#' => $txt['months'][5],
935
				'#txt_months_06#' => $txt['months'][6],
936
				'#txt_months_07#' => $txt['months'][7],
937
				'#txt_months_08#' => $txt['months'][8],
938
				'#txt_months_09#' => $txt['months'][9],
939
				'#txt_months_10#' => $txt['months'][10],
940
				'#txt_months_11#' => $txt['months'][11],
941
				'#txt_months_12#' => $txt['months'][12],
942
			));
943
	}
944
945
	// Restore any literal percent characters, add the prefix, and we're done.
946
	return $prefix . str_replace('&#37;', '%', $timestring);
947
}
948
949
/**
950
 * Gets a version of a strftime() format that only shows the date or time components
951
 *
952
 * @param string $type Either 'date' or 'time'.
953
 * @param string $format A strftime() format to process. Defaults to $user_info['time_format'].
954
 * @return string A strftime() format string
955
 */
956
function get_date_or_time_format($type = '', $format = '')
957
{
958
	global $user_info, $modSettings;
959
	static $formats;
960
961
	// If the format is invalid, fall back to defaults.
962
	if (strpos($format, '%') === false)
963
		$format = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %k:%M');
964
965
	$orig_format = $format;
966
967
	// Have we already done this?
968
	if (isset($formats[$orig_format][$type]))
969
		return $formats[$orig_format][$type];
970
971
	if ($type === 'date')
972
	{
973
		$specifications = array(
974
			// Day
975
			'%a' => '%a', '%A' => '%A', '%e' => '%e', '%d' => '%d', '%j' => '%j', '%u' => '%u', '%w' => '%w',
976
			// Week
977
			'%U' => '%U', '%V' => '%V', '%W' => '%W',
978
			// Month
979
			'%b' => '%b', '%B' => '%B', '%h' => '%h', '%m' => '%m',
980
			// Year
981
			'%C' => '%C', '%g' => '%g', '%G' => '%G', '%y' => '%y', '%Y' => '%Y',
982
			// Time
983
			'%H' => '', '%k' => '', '%I' => '', '%l' => '', '%M' => '', '%p' => '', '%P' => '',
984
			'%r' => '', '%R' => '', '%S' => '', '%T' => '', '%X' => '', '%z' => '', '%Z' => '',
985
			// Time and Date Stamps
986
			'%c' => '%x', '%D' => '%D', '%F' => '%F', '%s' => '%s', '%x' => '%x',
987
			// Miscellaneous
988
			'%n' => '', '%t' => '', '%%' => '%%',
989
		);
990
991
		$default_format = '%F';
992
	}
993
	elseif ($type === 'time')
994
	{
995
		$specifications = array(
996
			// Day
997
			'%a' => '', '%A' => '', '%e' => '', '%d' => '', '%j' => '', '%u' => '', '%w' => '',
998
			// Week
999
			'%U' => '', '%V' => '', '%W' => '',
1000
			// Month
1001
			'%b' => '', '%B' => '', '%h' => '', '%m' => '',
1002
			// Year
1003
			'%C' => '', '%g' => '', '%G' => '', '%y' => '', '%Y' => '',
1004
			// Time
1005
			'%H' => '%H', '%k' => '%k', '%I' => '%I', '%l' => '%l', '%M' => '%M', '%p' => '%p', '%P' => '%P',
1006
			'%r' => '%r', '%R' => '%R', '%S' => '%S', '%T' => '%T', '%X' => '%X', '%z' => '%z', '%Z' => '%Z',
1007
			// Time and Date Stamps
1008
			'%c' => '%X', '%D' => '', '%F' => '', '%s' => '%s', '%x' => '',
1009
			// Miscellaneous
1010
			'%n' => '', '%t' => '', '%%' => '%%',
1011
		);
1012
1013
		$default_format = '%k:%M';
1014
	}
1015
	// Invalid type requests just get the full format string.
1016
	else
1017
		return $format;
1018
1019
	// Separate the specifications we want from the ones we don't.
1020
	$wanted = array_filter($specifications);
1021
	$unwanted = array_diff(array_keys($specifications), $wanted);
1022
1023
	// First, make any necessary substitutions in the format.
1024
	$format = strtr($format, $wanted);
1025
1026
	// Next, strip out any specifications and literal text that we don't want.
1027
	$format_parts = preg_split('~%[' . (strtr(implode('', $unwanted), array('%' => ''))) . ']~u', $format);
1028
1029
	foreach ($format_parts as $p => $f)
1030
	{
1031
		if (strpos($f, '%') === false)
1032
			unset($format_parts[$p]);
1033
	}
1034
1035
	$format = implode('', $format_parts);
1036
1037
	// Finally, strip out any unwanted leftovers.
1038
	// 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
1039
	$format = preg_replace(
1040
		array(
1041
			// Anything that isn't a specification, punctuation mark, or whitespace.
1042
			'~(?<!%)\p{L}|[^\p{L}\p{P}\s]~u',
1043
			// A series of punctuation marks (except %), possibly separated by whitespace.
1044
			'~([^%\P{P}])(\s*)(?'.'>(\1|[^%\P{Po}])\s*(?!$))*~u',
1045
			// Unwanted trailing punctuation and whitespace.
1046
			'~(?'.'>([\p{Pd}\p{Ps}\p{Pi}\p{Pc}]|[^%\P{Po}])\s*)*$~u',
1047
			// Unwanted opening punctuation and whitespace.
1048
			'~^\s*(?'.'>([\p{Pd}\p{Pe}\p{Pf}\p{Pc}]|[^%\P{Po}])\s*)*~u',
1049
		),
1050
		array(
1051
			'',
1052
			'$1$2',
1053
			'',
1054
			'',
1055
		),
1056
		$format
1057
	);
1058
1059
	// Gotta have something...
1060
	if (empty($format))
1061
		$format = $default_format;
1062
1063
	// Remember what we've done.
1064
	$formats[$orig_format][$type] = trim($format);
1065
1066
	return $formats[$orig_format][$type];
1067
}
1068
1069
/**
1070
 * Replaces special entities in strings with the real characters.
1071
 *
1072
 * Functionally equivalent to htmlspecialchars_decode(), except that this also
1073
 * replaces '&nbsp;' with a simple space character.
1074
 *
1075
 * @param string $string A string
1076
 * @return string The string without entities
1077
 */
1078
function un_htmlspecialchars($string)
1079
{
1080
	global $context;
1081
	static $translation = array();
1082
1083
	// Determine the character set... Default to UTF-8
1084
	if (empty($context['character_set']))
1085
		$charset = 'UTF-8';
1086
	// Use ISO-8859-1 in place of non-supported ISO-8859 charsets...
1087
	elseif (strpos($context['character_set'], 'ISO-8859-') !== false && !in_array($context['character_set'], array('ISO-8859-5', 'ISO-8859-15')))
1088
		$charset = 'ISO-8859-1';
1089
	else
1090
		$charset = $context['character_set'];
1091
1092
	if (empty($translation))
1093
		$translation = array_flip(get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES, $charset)) + array('&#039;' => '\'', '&#39;' => '\'', '&nbsp;' => ' ');
1094
1095
	return strtr($string, $translation);
1096
}
1097
1098
/**
1099
 * Shorten a subject + internationalization concerns.
1100
 *
1101
 * - shortens a subject so that it is either shorter than length, or that length plus an ellipsis.
1102
 * - respects internationalization characters and entities as one character.
1103
 * - avoids trailing entities.
1104
 * - returns the shortened string.
1105
 *
1106
 * @param string $subject The subject
1107
 * @param int $len How many characters to limit it to
1108
 * @return string The shortened subject - either the entire subject (if it's <= $len) or the subject shortened to $len characters with "..." appended
1109
 */
1110
function shorten_subject($subject, $len)
1111
{
1112
	global $smcFunc;
1113
1114
	// It was already short enough!
1115
	if ($smcFunc['strlen']($subject) <= $len)
1116
		return $subject;
1117
1118
	// Shorten it by the length it was too long, and strip off junk from the end.
1119
	return $smcFunc['substr']($subject, 0, $len) . '...';
1120
}
1121
1122
/**
1123
 * Gets the current time with offset.
1124
 *
1125
 * - always applies the offset in the time_offset setting.
1126
 *
1127
 * @param bool $use_user_offset Whether to apply the user's offset as well
1128
 * @param int $timestamp A timestamp (null to use current time)
1129
 * @return int Seconds since the unix epoch, with forum time offset and (optionally) user time offset applied
1130
 */
1131
function forum_time($use_user_offset = true, $timestamp = null)
1132
{
1133
	global $user_info, $modSettings;
1134
1135
	// Ensure required values are set
1136
	$modSettings['time_offset'] = !empty($modSettings['time_offset']) ? $modSettings['time_offset'] : 0;
1137
	$user_info['time_offset'] = !empty($user_info['time_offset']) ? $user_info['time_offset'] : 0;
1138
1139
	if ($timestamp === null)
1140
		$timestamp = time();
1141
	elseif ($timestamp == 0)
1142
		return 0;
1143
1144
	return $timestamp + ($modSettings['time_offset'] + ($use_user_offset ? $user_info['time_offset'] : 0)) * 3600;
1145
}
1146
1147
/**
1148
 * Calculates all the possible permutations (orders) of array.
1149
 * should not be called on huge arrays (bigger than like 10 elements.)
1150
 * returns an array containing each permutation.
1151
 *
1152
 * @deprecated since 2.1
1153
 * @param array $array An array
1154
 * @return array An array containing each permutation
1155
 */
1156
function permute($array)
1157
{
1158
	$orders = array($array);
1159
1160
	$n = count($array);
1161
	$p = range(0, $n);
1162
	for ($i = 1; $i < $n; null)
1163
	{
1164
		$p[$i]--;
1165
		$j = $i % 2 != 0 ? $p[$i] : 0;
1166
1167
		$temp = $array[$i];
1168
		$array[$i] = $array[$j];
1169
		$array[$j] = $temp;
1170
1171
		for ($i = 1; $p[$i] == 0; $i++)
1172
			$p[$i] = 1;
1173
1174
		$orders[] = $array;
1175
	}
1176
1177
	return $orders;
1178
}
1179
1180
/**
1181
 * Parse bulletin board code in a string, as well as smileys optionally.
1182
 *
1183
 * - only parses bbc tags which are not disabled in disabledBBC.
1184
 * - handles basic HTML, if enablePostHTML is on.
1185
 * - caches the from/to replace regular expressions so as not to reload them every time a string is parsed.
1186
 * - only parses smileys if smileys is true.
1187
 * - does nothing if the enableBBC setting is off.
1188
 * - uses the cache_id as a unique identifier to facilitate any caching it may do.
1189
 * - returns the modified message.
1190
 *
1191
 * @param string|bool $message The message.
1192
 *		When a empty string, nothing is done.
1193
 *		When false we provide a list of BBC codes available.
1194
 *		When a string, the message is parsed and bbc handled.
1195
 * @param bool $smileys Whether to parse smileys as well
1196
 * @param string $cache_id The cache ID
1197
 * @param array $parse_tags If set, only parses these tags rather than all of them
1198
 * @return string The parsed message
1199
 */
1200
function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = array())
1201
{
1202
	global $smcFunc, $txt, $scripturl, $context, $modSettings, $user_info, $sourcedir, $cache_enable;
1203
	static $bbc_lang_locales = array(), $itemcodes = array(), $no_autolink_tags = array();
1204
	static $disabled, $alltags_regex = '', $param_regexes = array(), $url_regex = '';
1205
1206
	// Don't waste cycles
1207
	if ($message === '')
1208
		return '';
1209
1210
	// Just in case it wasn't determined yet whether UTF-8 is enabled.
1211
	if (!isset($context['utf8']))
1212
		$context['utf8'] = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8';
1213
1214
	// Clean up any cut/paste issues we may have
1215
	$message = sanitizeMSCutPaste($message);
1216
1217
	// If the load average is too high, don't parse the BBC.
1218
	if (!empty($context['load_average']) && !empty($modSettings['bbc']) && $context['load_average'] >= $modSettings['bbc'])
1219
	{
1220
		$context['disabled_parse_bbc'] = true;
1221
		return $message;
1222
	}
1223
1224
	if ($smileys !== null && ($smileys == '1' || $smileys == '0'))
1225
		$smileys = (bool) $smileys;
1226
1227
	if (empty($modSettings['enableBBC']) && $message !== false)
1228
	{
1229
		if ($smileys === true)
1230
			parsesmileys($message);
1231
1232
		return $message;
1233
	}
1234
1235
	// If we already have a version of the BBCodes for the current language, use that. Otherwise, make one.
1236
	if (!empty($bbc_lang_locales[$txt['lang_locale']]))
1237
		$bbc_codes = $bbc_lang_locales[$txt['lang_locale']];
1238
	else
1239
		$bbc_codes = array();
1240
1241
	// If we are not doing every tag then we don't cache this run.
1242
	if (!empty($parse_tags))
1243
		$bbc_codes = array();
1244
1245
	// Ensure $modSettings['tld_regex'] contains a valid regex for the autolinker
1246
	if (!empty($modSettings['autoLinkUrls']))
1247
		set_tld_regex();
1248
1249
	// Allow mods access before entering the main parse_bbc loop
1250
	call_integration_hook('integrate_pre_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
1251
1252
	// Sift out the bbc for a performance improvement.
1253
	if (empty($bbc_codes) || $message === false || !empty($parse_tags))
1254
	{
1255
		if (!empty($modSettings['disabledBBC']))
1256
		{
1257
			$disabled = array();
1258
1259
			$temp = explode(',', strtolower($modSettings['disabledBBC']));
1260
1261
			foreach ($temp as $tag)
1262
				$disabled[trim($tag)] = true;
1263
1264
			if (in_array('color', $disabled))
1265
				$disabled = array_merge($disabled, array(
1266
					'black' => true,
1267
					'white' => true,
1268
					'red' => true,
1269
					'green' => true,
1270
					'blue' => true,
1271
					)
1272
				);
1273
		}
1274
1275
		// The YouTube bbc needs this for its origin parameter
1276
		$scripturl_parts = parse_url($scripturl);
1277
		$hosturl = $scripturl_parts['scheme'] . '://' . $scripturl_parts['host'];
1278
1279
		/* The following bbc are formatted as an array, with keys as follows:
1280
1281
			tag: the tag's name - should be lowercase!
1282
1283
			type: one of...
1284
				- (missing): [tag]parsed content[/tag]
1285
				- unparsed_equals: [tag=xyz]parsed content[/tag]
1286
				- parsed_equals: [tag=parsed data]parsed content[/tag]
1287
				- unparsed_content: [tag]unparsed content[/tag]
1288
				- closed: [tag], [tag/], [tag /]
1289
				- unparsed_commas: [tag=1,2,3]parsed content[/tag]
1290
				- unparsed_commas_content: [tag=1,2,3]unparsed content[/tag]
1291
				- unparsed_equals_content: [tag=...]unparsed content[/tag]
1292
1293
			parameters: an optional array of parameters, for the form
1294
			  [tag abc=123]content[/tag].  The array is an associative array
1295
			  where the keys are the parameter names, and the values are an
1296
			  array which may contain the following:
1297
				- match: a regular expression to validate and match the value.
1298
				- quoted: true if the value should be quoted.
1299
				- validate: callback to evaluate on the data, which is $data.
1300
				- value: a string in which to replace $1 with the data.
1301
					Either value or validate may be used, not both.
1302
				- optional: true if the parameter is optional.
1303
				- default: a default value for missing optional parameters.
1304
1305
			test: a regular expression to test immediately after the tag's
1306
			  '=', ' ' or ']'.  Typically, should have a \] at the end.
1307
			  Optional.
1308
1309
			content: only available for unparsed_content, closed,
1310
			  unparsed_commas_content, and unparsed_equals_content.
1311
			  $1 is replaced with the content of the tag.  Parameters
1312
			  are replaced in the form {param}.  For unparsed_commas_content,
1313
			  $2, $3, ..., $n are replaced.
1314
1315
			before: only when content is not used, to go before any
1316
			  content.  For unparsed_equals, $1 is replaced with the value.
1317
			  For unparsed_commas, $1, $2, ..., $n are replaced.
1318
1319
			after: similar to before in every way, except that it is used
1320
			  when the tag is closed.
1321
1322
			disabled_content: used in place of content when the tag is
1323
			  disabled.  For closed, default is '', otherwise it is '$1' if
1324
			  block_level is false, '<div>$1</div>' elsewise.
1325
1326
			disabled_before: used in place of before when disabled.  Defaults
1327
			  to '<div>' if block_level, '' if not.
1328
1329
			disabled_after: used in place of after when disabled.  Defaults
1330
			  to '</div>' if block_level, '' if not.
1331
1332
			block_level: set to true the tag is a "block level" tag, similar
1333
			  to HTML.  Block level tags cannot be nested inside tags that are
1334
			  not block level, and will not be implicitly closed as easily.
1335
			  One break following a block level tag may also be removed.
1336
1337
			trim: if set, and 'inside' whitespace after the begin tag will be
1338
			  removed.  If set to 'outside', whitespace after the end tag will
1339
			  meet the same fate.
1340
1341
			validate: except when type is missing or 'closed', a callback to
1342
			  validate the data as $data.  Depending on the tag's type, $data
1343
			  may be a string or an array of strings (corresponding to the
1344
			  replacement.)
1345
1346
			quoted: when type is 'unparsed_equals' or 'parsed_equals' only,
1347
			  may be not set, 'optional', or 'required' corresponding to if
1348
			  the content may be quoted.  This allows the parser to read
1349
			  [tag="abc]def[esdf]"] properly.
1350
1351
			require_parents: an array of tag names, or not set.  If set, the
1352
			  enclosing tag *must* be one of the listed tags, or parsing won't
1353
			  occur.
1354
1355
			require_children: similar to require_parents, if set children
1356
			  won't be parsed if they are not in the list.
1357
1358
			disallow_children: similar to, but very different from,
1359
			  require_children, if it is set the listed tags will not be
1360
			  parsed inside the tag.
1361
1362
			parsed_tags_allowed: an array restricting what BBC can be in the
1363
			  parsed_equals parameter, if desired.
1364
		*/
1365
1366
		$codes = array(
1367
			array(
1368
				'tag' => 'abbr',
1369
				'type' => 'unparsed_equals',
1370
				'before' => '<abbr title="$1">',
1371
				'after' => '</abbr>',
1372
				'quoted' => 'optional',
1373
				'disabled_after' => ' ($1)',
1374
			),
1375
			// Legacy (and just an alias for [abbr] even when enabled)
1376
			array(
1377
				'tag' => 'acronym',
1378
				'type' => 'unparsed_equals',
1379
				'before' => '<abbr title="$1">',
1380
				'after' => '</abbr>',
1381
				'quoted' => 'optional',
1382
				'disabled_after' => ' ($1)',
1383
			),
1384
			array(
1385
				'tag' => 'anchor',
1386
				'type' => 'unparsed_equals',
1387
				'test' => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]',
1388
				'before' => '<span id="post_$1">',
1389
				'after' => '</span>',
1390
			),
1391
			array(
1392
				'tag' => 'attach',
1393
				'type' => 'unparsed_content',
1394
				'parameters' => array(
1395
					'id' => array('match' => '(\d+)'),
1396
					'alt' => array('optional' => true),
1397
					'width' => array('optional' => true, 'match' => '(\d+)'),
1398
					'height' => array('optional' => true, 'match' => '(\d+)'),
1399
					'display' => array('optional' => true, 'match' => '(link|embed)'),
1400
				),
1401
				'content' => '$1',
1402
				'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...
1403
				{
1404
					$returnContext = '';
1405
1406
					// BBC or the entire attachments feature is disabled
1407
					if (empty($modSettings['attachmentEnable']) || !empty($disabled['attach']))
1408
						return $data;
1409
1410
					// Save the attach ID.
1411
					$attachID = $params['{id}'];
1412
1413
					// Kinda need this.
1414
					require_once($sourcedir . '/Subs-Attachments.php');
1415
1416
					$currentAttachment = parseAttachBBC($attachID);
1417
1418
					// parseAttachBBC will return a string ($txt key) rather than dying with a fatal_error. Up to you to decide what to do.
1419
					if (is_string($currentAttachment))
1420
						return $data = !empty($txt[$currentAttachment]) ? $txt[$currentAttachment] : $currentAttachment;
1421
1422
					// We need a display mode.
1423
					if (empty($params['{display}']))
1424
					{
1425
						// Images, video, and audio are embedded by default.
1426
						if (!empty($currentAttachment['is_image']) || strpos($currentAttachment['mime_type'], 'video/') === 0 || strpos($currentAttachment['mime_type'], 'audio/') === 0)
1427
							$params['{display}'] = 'embed';
1428
						// Anything else shows a link by default.
1429
						else
1430
							$params['{display}'] = 'link';
1431
					}
1432
1433
					// Embedded file.
1434
					if ($params['{display}'] == 'embed')
1435
					{
1436
						$alt = ' alt="' . (!empty($params['{alt}']) ? $params['{alt}'] : $currentAttachment['name']) . '"';
1437
						$title = !empty($data) ? ' title="' . $smcFunc['htmlspecialchars']($data) . '"' : '';
1438
1439
						// Image.
1440
						if (!empty($currentAttachment['is_image']))
1441
						{
1442
							if (empty($params['{width}']) && empty($params['{height}']))
1443
								$returnContext .= '<img src="' . $currentAttachment['href'] . '"' . $alt . $title . ' class="bbc_img"></a>';
1444
							else
1445
							{
1446
								$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"': '';
1447
								$height = !empty($params['{height}']) ? 'height="' . $params['{height}'] . '"' : '';
1448
								$returnContext .= '<img src="' . $currentAttachment['href'] . ';image"' . $alt . $title . $width . $height . ' class="bbc_img resized"/>';
1449
							}
1450
						}
1451
						// Video.
1452
						elseif (strpos($currentAttachment['mime_type'], 'video/') === 0)
1453
						{
1454
							$width = !empty($width) ? ' width="' . $width . '"' : '';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $width seems to never exist and therefore empty should always be true.
Loading history...
1455
							$height = !empty($height) ? ' height="' . $height . '"' : '';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $height seems to never exist and therefore empty should always be true.
Loading history...
1456
1457
							$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>' : '');
1458
						}
1459
						// Audio.
1460
						elseif (strpos($currentAttachment['mime_type'], 'audio/') === 0)
1461
						{
1462
							$width = 'max-width:100%; width: ' . (!empty($width) ? $width : '400') . 'px;';
1463
							$height = !empty($height) ? 'height: ' . $height . 'px;' : '';
1464
1465
							$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>';
1466
						}
1467
						// Anything else.
1468
						else
1469
						{
1470
							$width = !empty($width) ? ' width="' . $width . '"' : '';
1471
							$height = !empty($height) ? ' height="' . $height . '"' : '';
1472
1473
							$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>';
1474
						}
1475
					}
1476
1477
					// No image. Show a link.
1478
					else
1479
						$returnContext .= '<a href="' . $currentAttachment['href'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . '</a>';
1480
1481
					// Use this hook to adjust the HTML output of the attach BBCode.
1482
					// If you want to work with the attachment data itself, use one of these:
1483
					// - integrate_pre_parseAttachBBC
1484
					// - integrate_post_parseAttachBBC
1485
					call_integration_hook('integrate_attach_bbc_validate', array(&$returnContext, $currentAttachment, $tag, $data, $disabled, $params));
1486
1487
					// Gotta append what we just did.
1488
					$data = $returnContext;
1489
				},
1490
			),
1491
			array(
1492
				'tag' => 'b',
1493
				'before' => '<b>',
1494
				'after' => '</b>',
1495
			),
1496
			// Legacy (equivalent to [ltr] or [rtl])
1497
			array(
1498
				'tag' => 'bdo',
1499
				'type' => 'unparsed_equals',
1500
				'before' => '<bdo dir="$1">',
1501
				'after' => '</bdo>',
1502
				'test' => '(rtl|ltr)\]',
1503
				'block_level' => true,
1504
			),
1505
			// Legacy (alias of [color=black])
1506
			array(
1507
				'tag' => 'black',
1508
				'before' => '<span style="color: black;" class="bbc_color">',
1509
				'after' => '</span>',
1510
			),
1511
			// Legacy (alias of [color=blue])
1512
			array(
1513
				'tag' => 'blue',
1514
				'before' => '<span style="color: blue;" class="bbc_color">',
1515
				'after' => '</span>',
1516
			),
1517
			array(
1518
				'tag' => 'br',
1519
				'type' => 'closed',
1520
				'content' => '<br>',
1521
			),
1522
			array(
1523
				'tag' => 'center',
1524
				'before' => '<div class="centertext">',
1525
				'after' => '</div>',
1526
				'block_level' => true,
1527
			),
1528
			array(
1529
				'tag' => 'code',
1530
				'type' => 'unparsed_content',
1531
				'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>',
1532
				// @todo Maybe this can be simplified?
1533
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1534
				{
1535
					if (!isset($disabled['code']))
1536
					{
1537
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
1538
1539
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
1540
						{
1541
							// Do PHP code coloring?
1542
							if ($php_parts[$php_i] != '&lt;?php')
1543
								continue;
1544
1545
							$php_string = '';
1546
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1547
							{
1548
								$php_string .= $php_parts[$php_i];
1549
								$php_parts[$php_i++] = '';
1550
							}
1551
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1552
						}
1553
1554
						// Fix the PHP code stuff...
1555
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1556
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1557
1558
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1559
						if (!empty($context['browser']['is_opera']))
1560
							$data .= '&nbsp;';
1561
					}
1562
				},
1563
				'block_level' => true,
1564
			),
1565
			array(
1566
				'tag' => 'code',
1567
				'type' => 'unparsed_equals_content',
1568
				'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>',
1569
				// @todo Maybe this can be simplified?
1570
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1571
				{
1572
					if (!isset($disabled['code']))
1573
					{
1574
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1575
1576
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
1577
						{
1578
							// Do PHP code coloring?
1579
							if ($php_parts[$php_i] != '&lt;?php')
1580
								continue;
1581
1582
							$php_string = '';
1583
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1584
							{
1585
								$php_string .= $php_parts[$php_i];
1586
								$php_parts[$php_i++] = '';
1587
							}
1588
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1589
						}
1590
1591
						// Fix the PHP code stuff...
1592
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1593
						$data[0] = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data[0]);
1594
1595
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1596
						if (!empty($context['browser']['is_opera']))
1597
							$data[0] .= '&nbsp;';
1598
					}
1599
				},
1600
				'block_level' => true,
1601
			),
1602
			array(
1603
				'tag' => 'color',
1604
				'type' => 'unparsed_equals',
1605
				'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]?)\))\]',
1606
				'before' => '<span style="color: $1;" class="bbc_color">',
1607
				'after' => '</span>',
1608
			),
1609
			array(
1610
				'tag' => 'email',
1611
				'type' => 'unparsed_content',
1612
				'content' => '<a href="mailto:$1" class="bbc_email">$1</a>',
1613
				// @todo Should this respect guest_hideContacts?
1614
				'validate' => function(&$tag, &$data, $disabled)
1615
				{
1616
					$data = strtr($data, array('<br>' => ''));
1617
				},
1618
			),
1619
			array(
1620
				'tag' => 'email',
1621
				'type' => 'unparsed_equals',
1622
				'before' => '<a href="mailto:$1" class="bbc_email">',
1623
				'after' => '</a>',
1624
				// @todo Should this respect guest_hideContacts?
1625
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1626
				'disabled_after' => ' ($1)',
1627
			),
1628
			// Legacy (and just a link even when not disabled)
1629
			array(
1630
				'tag' => 'flash',
1631
				'type' => 'unparsed_commas_content',
1632
				'test' => '\d+,\d+\]',
1633
				'content' => '<a href="$1" target="_blank" rel="noopener">$1</a>',
1634
				'validate' => function (&$tag, &$data, $disabled)
1635
				{
1636
					$scheme = parse_url($data[0], PHP_URL_SCHEME);
1637
					if (empty($scheme))
1638
						$data[0] = '//' . ltrim($data[0], ':/');
1639
				},
1640
			),
1641
			array(
1642
				'tag' => 'float',
1643
				'type' => 'unparsed_equals',
1644
				'test' => '(left|right)(\s+max=\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)?\]',
1645
				'before' => '<div $1>',
1646
				'after' => '</div>',
1647
				'validate' => function(&$tag, &$data, $disabled)
1648
				{
1649
					$class = 'class="bbc_float float' . (strpos($data, 'left') === 0 ? 'left' : 'right') . '"';
1650
1651
					if (preg_match('~\bmax=(\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)~', $data, $matches))
1652
						$css = ' style="max-width:' . $matches[1] . (is_numeric($matches[1]) ? 'px' : '') . '"';
1653
					else
1654
						$css = '';
1655
1656
					$data = $class . $css;
1657
				},
1658
				'trim' => 'outside',
1659
				'block_level' => true,
1660
			),
1661
			// Legacy (alias of [url] with an FTP URL)
1662
			array(
1663
				'tag' => 'ftp',
1664
				'type' => 'unparsed_content',
1665
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
1666
				'validate' => function(&$tag, &$data, $disabled)
1667
				{
1668
					$data = strtr($data, array('<br>' => ''));
1669
					$scheme = parse_url($data, PHP_URL_SCHEME);
1670
					if (empty($scheme))
1671
						$data = 'ftp://' . ltrim($data, ':/');
1672
				},
1673
			),
1674
			// Legacy (alias of [url] with an FTP URL)
1675
			array(
1676
				'tag' => 'ftp',
1677
				'type' => 'unparsed_equals',
1678
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
1679
				'after' => '</a>',
1680
				'validate' => function(&$tag, &$data, $disabled)
1681
				{
1682
					$scheme = parse_url($data, PHP_URL_SCHEME);
1683
					if (empty($scheme))
1684
						$data = 'ftp://' . ltrim($data, ':/');
1685
				},
1686
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1687
				'disabled_after' => ' ($1)',
1688
			),
1689
			array(
1690
				'tag' => 'font',
1691
				'type' => 'unparsed_equals',
1692
				'test' => '[A-Za-z0-9_,\-\s]+?\]',
1693
				'before' => '<span style="font-family: $1;" class="bbc_font">',
1694
				'after' => '</span>',
1695
			),
1696
			// Legacy (one of those things that should not be done)
1697
			array(
1698
				'tag' => 'glow',
1699
				'type' => 'unparsed_commas',
1700
				'test' => '[#0-9a-zA-Z\-]{3,12},([012]\d{1,2}|\d{1,2})(,[^]]+)?\]',
1701
				'before' => '<span style="text-shadow: $1 1px 1px 1px">',
1702
				'after' => '</span>',
1703
			),
1704
			// Legacy (alias of [color=green])
1705
			array(
1706
				'tag' => 'green',
1707
				'before' => '<span style="color: green;" class="bbc_color">',
1708
				'after' => '</span>',
1709
			),
1710
			array(
1711
				'tag' => 'html',
1712
				'type' => 'unparsed_content',
1713
				'content' => '<div>$1</div>',
1714
				'block_level' => true,
1715
				'disabled_content' => '$1',
1716
			),
1717
			array(
1718
				'tag' => 'hr',
1719
				'type' => 'closed',
1720
				'content' => '<hr>',
1721
				'block_level' => true,
1722
			),
1723
			array(
1724
				'tag' => 'i',
1725
				'before' => '<i>',
1726
				'after' => '</i>',
1727
			),
1728
			array(
1729
				'tag' => 'img',
1730
				'type' => 'unparsed_content',
1731
				'parameters' => array(
1732
					'alt' => array('optional' => true),
1733
					'title' => array('optional' => true),
1734
				),
1735
				'content' => '<img src="$1" alt="{alt}" title="{title}" class="bbc_img" loading="lazy">',
1736
				'validate' => function(&$tag, &$data, $disabled)
1737
				{
1738
					$data = strtr($data, array('<br>' => ''));
1739
1740
					if (parse_url($data, PHP_URL_SCHEME) === null)
1741
						$data = '//' . ltrim($data, ':/');
1742
					else
1743
						$data = get_proxied_url($data);
1744
				},
1745
				'disabled_content' => '($1)',
1746
			),
1747
			array(
1748
				'tag' => 'img',
1749
				'type' => 'unparsed_content',
1750
				'parameters' => array(
1751
					'alt' => array('optional' => true),
1752
					'title' => array('optional' => true),
1753
					'width' => array('optional' => true, 'value' => ' width="$1"', 'match' => '(\d+)'),
1754
					'height' => array('optional' => true, 'value' => ' height="$1"', 'match' => '(\d+)'),
1755
				),
1756
				'content' => '<img src="$1" alt="{alt}" title="{title}"{width}{height} class="bbc_img resized" loading="lazy">',
1757
				'validate' => function(&$tag, &$data, $disabled)
1758
				{
1759
					$data = strtr($data, array('<br>' => ''));
1760
1761
					if (parse_url($data, PHP_URL_SCHEME) === null)
1762
						$data = '//' . ltrim($data, ':/');
1763
					else
1764
						$data = get_proxied_url($data);
1765
				},
1766
				'disabled_content' => '($1)',
1767
			),
1768
			array(
1769
				'tag' => 'iurl',
1770
				'type' => 'unparsed_content',
1771
				'content' => '<a href="$1" class="bbc_link">$1</a>',
1772
				'validate' => function(&$tag, &$data, $disabled)
1773
				{
1774
					$data = strtr($data, array('<br>' => ''));
1775
					$scheme = parse_url($data, PHP_URL_SCHEME);
1776
					if (empty($scheme))
1777
						$data = '//' . ltrim($data, ':/');
1778
				},
1779
			),
1780
			array(
1781
				'tag' => 'iurl',
1782
				'type' => 'unparsed_equals',
1783
				'quoted' => 'optional',
1784
				'before' => '<a href="$1" class="bbc_link">',
1785
				'after' => '</a>',
1786
				'validate' => function(&$tag, &$data, $disabled)
1787
				{
1788
					if (substr($data, 0, 1) == '#')
1789
						$data = '#post_' . substr($data, 1);
1790
					else
1791
					{
1792
						$scheme = parse_url($data, PHP_URL_SCHEME);
1793
						if (empty($scheme))
1794
							$data = '//' . ltrim($data, ':/');
1795
					}
1796
				},
1797
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1798
				'disabled_after' => ' ($1)',
1799
			),
1800
			array(
1801
				'tag' => 'justify',
1802
				'before' => '<div class="justifytext">',
1803
				'after' => '</div>',
1804
				'block_level' => true,
1805
			),
1806
			array(
1807
				'tag' => 'left',
1808
				'before' => '<div class="lefttext">',
1809
				'after' => '</div>',
1810
				'block_level' => true,
1811
			),
1812
			array(
1813
				'tag' => 'li',
1814
				'before' => '<li>',
1815
				'after' => '</li>',
1816
				'trim' => 'outside',
1817
				'require_parents' => array('list'),
1818
				'block_level' => true,
1819
				'disabled_before' => '',
1820
				'disabled_after' => '<br>',
1821
			),
1822
			array(
1823
				'tag' => 'list',
1824
				'before' => '<ul class="bbc_list">',
1825
				'after' => '</ul>',
1826
				'trim' => 'inside',
1827
				'require_children' => array('li', 'list'),
1828
				'block_level' => true,
1829
			),
1830
			array(
1831
				'tag' => 'list',
1832
				'parameters' => array(
1833
					'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)'),
1834
				),
1835
				'before' => '<ul class="bbc_list" style="list-style-type: {type};">',
1836
				'after' => '</ul>',
1837
				'trim' => 'inside',
1838
				'require_children' => array('li'),
1839
				'block_level' => true,
1840
			),
1841
			array(
1842
				'tag' => 'ltr',
1843
				'before' => '<bdo dir="ltr">',
1844
				'after' => '</bdo>',
1845
				'block_level' => true,
1846
			),
1847
			array(
1848
				'tag' => 'me',
1849
				'type' => 'unparsed_equals',
1850
				'before' => '<div class="meaction">* $1 ',
1851
				'after' => '</div>',
1852
				'quoted' => 'optional',
1853
				'block_level' => true,
1854
				'disabled_before' => '/me ',
1855
				'disabled_after' => '<br>',
1856
			),
1857
			array(
1858
				'tag' => 'member',
1859
				'type' => 'unparsed_equals',
1860
				'before' => '<a href="' . $scripturl . '?action=profile;u=$1" class="mention" data-mention="$1">@',
1861
				'after' => '</a>',
1862
			),
1863
			// Legacy (horrible memories of the 1990s)
1864
			array(
1865
				'tag' => 'move',
1866
				'before' => '<marquee>',
1867
				'after' => '</marquee>',
1868
				'block_level' => true,
1869
				'disallow_children' => array('move'),
1870
			),
1871
			array(
1872
				'tag' => 'nobbc',
1873
				'type' => 'unparsed_content',
1874
				'content' => '$1',
1875
			),
1876
			array(
1877
				'tag' => 'php',
1878
				'type' => 'unparsed_content',
1879
				'content' => '<span class="phpcode">$1</span>',
1880
				'validate' => isset($disabled['php']) ? null : function(&$tag, &$data, $disabled)
1881
				{
1882
					if (!isset($disabled['php']))
1883
					{
1884
						$add_begin = substr(trim($data), 0, 5) != '&lt;?';
1885
						$data = highlight_php_code($add_begin ? '&lt;?php ' . $data . '?&gt;' : $data);
1886
						if ($add_begin)
1887
							$data = preg_replace(array('~^(.+?)&lt;\?.{0,40}?php(?:&nbsp;|\s)~', '~\?&gt;((?:</(font|span)>)*)$~'), '$1', $data, 2);
1888
					}
1889
				},
1890
				'block_level' => false,
1891
				'disabled_content' => '$1',
1892
			),
1893
			array(
1894
				'tag' => 'pre',
1895
				'before' => '<pre>',
1896
				'after' => '</pre>',
1897
			),
1898
			array(
1899
				'tag' => 'quote',
1900
				'before' => '<blockquote><cite>' . $txt['quote'] . '</cite>',
1901
				'after' => '</blockquote>',
1902
				'trim' => 'both',
1903
				'block_level' => true,
1904
			),
1905
			array(
1906
				'tag' => 'quote',
1907
				'parameters' => array(
1908
					'author' => array('match' => '(.{1,192}?)', 'quoted' => true),
1909
				),
1910
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1911
				'after' => '</blockquote>',
1912
				'trim' => 'both',
1913
				'block_level' => true,
1914
			),
1915
			array(
1916
				'tag' => 'quote',
1917
				'type' => 'parsed_equals',
1918
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': $1</cite>',
1919
				'after' => '</blockquote>',
1920
				'trim' => 'both',
1921
				'quoted' => 'optional',
1922
				// Don't allow everything to be embedded with the author name.
1923
				'parsed_tags_allowed' => array('url', 'iurl', 'ftp'),
1924
				'block_level' => true,
1925
			),
1926
			array(
1927
				'tag' => 'quote',
1928
				'parameters' => array(
1929
					'author' => array('match' => '([^<>]{1,192}?)'),
1930
					'link' => array('match' => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|msg=\d+?|action=profile;u=\d+)'),
1931
					'date' => array('match' => '(\d+)', 'validate' => 'timeformat'),
1932
				),
1933
				'before' => '<blockquote><cite><a href="' . $scripturl . '?{link}">' . $txt['quote_from'] . ': {author} ' . $txt['search_on'] . ' {date}</a></cite>',
1934
				'after' => '</blockquote>',
1935
				'trim' => 'both',
1936
				'block_level' => true,
1937
			),
1938
			array(
1939
				'tag' => 'quote',
1940
				'parameters' => array(
1941
					'author' => array('match' => '(.{1,192}?)'),
1942
				),
1943
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1944
				'after' => '</blockquote>',
1945
				'trim' => 'both',
1946
				'block_level' => true,
1947
			),
1948
			// Legacy (alias of [color=red])
1949
			array(
1950
				'tag' => 'red',
1951
				'before' => '<span style="color: red;" class="bbc_color">',
1952
				'after' => '</span>',
1953
			),
1954
			array(
1955
				'tag' => 'right',
1956
				'before' => '<div class="righttext">',
1957
				'after' => '</div>',
1958
				'block_level' => true,
1959
			),
1960
			array(
1961
				'tag' => 'rtl',
1962
				'before' => '<bdo dir="rtl">',
1963
				'after' => '</bdo>',
1964
				'block_level' => true,
1965
			),
1966
			array(
1967
				'tag' => 's',
1968
				'before' => '<s>',
1969
				'after' => '</s>',
1970
			),
1971
			// Legacy (never a good idea)
1972
			array(
1973
				'tag' => 'shadow',
1974
				'type' => 'unparsed_commas',
1975
				'test' => '[#0-9a-zA-Z\-]{3,12},(left|right|top|bottom|[0123]\d{0,2})\]',
1976
				'before' => '<span style="text-shadow: $1 $2">',
1977
				'after' => '</span>',
1978
				'validate' => function(&$tag, &$data, $disabled)
1979
				{
1980
1981
					if ($data[1] == 'top' || (is_numeric($data[1]) && $data[1] < 50))
1982
						$data[1] = '0 -2px 1px';
1983
1984
					elseif ($data[1] == 'right' || (is_numeric($data[1]) && $data[1] < 100))
1985
						$data[1] = '2px 0 1px';
1986
1987
					elseif ($data[1] == 'bottom' || (is_numeric($data[1]) && $data[1] < 190))
1988
						$data[1] = '0 2px 1px';
1989
1990
					elseif ($data[1] == 'left' || (is_numeric($data[1]) && $data[1] < 280))
1991
						$data[1] = '-2px 0 1px';
1992
1993
					else
1994
						$data[1] = '1px 1px 1px';
1995
				},
1996
			),
1997
			array(
1998
				'tag' => 'size',
1999
				'type' => 'unparsed_equals',
2000
				'test' => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)\]',
2001
				'before' => '<span style="font-size: $1;" class="bbc_size">',
2002
				'after' => '</span>',
2003
			),
2004
			array(
2005
				'tag' => 'size',
2006
				'type' => 'unparsed_equals',
2007
				'test' => '[1-7]\]',
2008
				'before' => '<span style="font-size: $1;" class="bbc_size">',
2009
				'after' => '</span>',
2010
				'validate' => function(&$tag, &$data, $disabled)
2011
				{
2012
					$sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95);
2013
					$data = $sizes[$data] . 'em';
2014
				},
2015
			),
2016
			array(
2017
				'tag' => 'sub',
2018
				'before' => '<sub>',
2019
				'after' => '</sub>',
2020
			),
2021
			array(
2022
				'tag' => 'sup',
2023
				'before' => '<sup>',
2024
				'after' => '</sup>',
2025
			),
2026
			array(
2027
				'tag' => 'table',
2028
				'before' => '<table class="bbc_table">',
2029
				'after' => '</table>',
2030
				'trim' => 'inside',
2031
				'require_children' => array('tr'),
2032
				'block_level' => true,
2033
			),
2034
			array(
2035
				'tag' => 'td',
2036
				'before' => '<td>',
2037
				'after' => '</td>',
2038
				'require_parents' => array('tr'),
2039
				'trim' => 'outside',
2040
				'block_level' => true,
2041
				'disabled_before' => '',
2042
				'disabled_after' => '',
2043
			),
2044
			array(
2045
				'tag' => 'time',
2046
				'type' => 'unparsed_content',
2047
				'content' => '$1',
2048
				'validate' => function(&$tag, &$data, $disabled)
2049
				{
2050
					if (is_numeric($data))
2051
						$data = timeformat($data);
2052
2053
					$tag['content'] = '<span class="bbc_time">$1</span>';
2054
				},
2055
			),
2056
			array(
2057
				'tag' => 'tr',
2058
				'before' => '<tr>',
2059
				'after' => '</tr>',
2060
				'require_parents' => array('table'),
2061
				'require_children' => array('td'),
2062
				'trim' => 'both',
2063
				'block_level' => true,
2064
				'disabled_before' => '',
2065
				'disabled_after' => '',
2066
			),
2067
			// Legacy (the <tt> element is dead)
2068
			array(
2069
				'tag' => 'tt',
2070
				'before' => '<span class="monospace">',
2071
				'after' => '</span>',
2072
			),
2073
			array(
2074
				'tag' => 'u',
2075
				'before' => '<u>',
2076
				'after' => '</u>',
2077
			),
2078
			array(
2079
				'tag' => 'url',
2080
				'type' => 'unparsed_content',
2081
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
2082
				'validate' => function(&$tag, &$data, $disabled)
2083
				{
2084
					$data = strtr($data, array('<br>' => ''));
2085
					$scheme = parse_url($data, PHP_URL_SCHEME);
2086
					if (empty($scheme))
2087
						$data = '//' . ltrim($data, ':/');
2088
				},
2089
			),
2090
			array(
2091
				'tag' => 'url',
2092
				'type' => 'unparsed_equals',
2093
				'quoted' => 'optional',
2094
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
2095
				'after' => '</a>',
2096
				'validate' => function(&$tag, &$data, $disabled)
2097
				{
2098
					$scheme = parse_url($data, PHP_URL_SCHEME);
2099
					if (empty($scheme))
2100
						$data = '//' . ltrim($data, ':/');
2101
				},
2102
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
2103
				'disabled_after' => ' ($1)',
2104
			),
2105
			// Legacy (alias of [color=white])
2106
			array(
2107
				'tag' => 'white',
2108
				'before' => '<span style="color: white;" class="bbc_color">',
2109
				'after' => '</span>',
2110
			),
2111
			array(
2112
				'tag' => 'youtube',
2113
				'type' => 'unparsed_content',
2114
				'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>',
2115
				'disabled_content' => '<a href="https://www.youtube.com/watch?v=$1" target="_blank" rel="noopener">https://www.youtube.com/watch?v=$1</a>',
2116
				'block_level' => true,
2117
			),
2118
		);
2119
2120
		// Inside these tags autolink is not recommendable.
2121
		$no_autolink_tags = array(
2122
			'url',
2123
			'iurl',
2124
			'email',
2125
			'img',
2126
			'html',
2127
		);
2128
2129
		// Let mods add new BBC without hassle.
2130
		call_integration_hook('integrate_bbc_codes', array(&$codes, &$no_autolink_tags));
2131
2132
		// This is mainly for the bbc manager, so it's easy to add tags above.  Custom BBC should be added above this line.
2133
		if ($message === false)
0 ignored issues
show
introduced by
The condition $message === false is always false.
Loading history...
2134
		{
2135
			usort($codes, function($a, $b)
2136
			{
2137
				return strcmp($a['tag'], $b['tag']);
2138
			});
2139
			return $codes;
2140
		}
2141
2142
		// So the parser won't skip them.
2143
		$itemcodes = array(
2144
			'*' => 'disc',
2145
			'@' => 'disc',
2146
			'+' => 'square',
2147
			'x' => 'square',
2148
			'#' => 'square',
2149
			'o' => 'circle',
2150
			'O' => 'circle',
2151
			'0' => 'circle',
2152
		);
2153
		if (!isset($disabled['li']) && !isset($disabled['list']))
2154
		{
2155
			foreach ($itemcodes as $c => $dummy)
2156
				$bbc_codes[$c] = array();
2157
		}
2158
2159
		// Shhhh!
2160
		if (!isset($disabled['color']))
2161
		{
2162
			$codes[] = array(
2163
				'tag' => 'chrissy',
2164
				'before' => '<span style="color: #cc0099;">',
2165
				'after' => ' :-*</span>',
2166
			);
2167
			$codes[] = array(
2168
				'tag' => 'kissy',
2169
				'before' => '<span style="color: #cc0099;">',
2170
				'after' => ' :-*</span>',
2171
			);
2172
		}
2173
		$codes[] = array(
2174
			'tag' => 'cowsay',
2175
			'parameters' => array(
2176
				'e' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => 'oo', 'validate' => function ($eyes) use ($smcFunc)
2177
					{
2178
						return $smcFunc['substr']($eyes . 'oo', 0, 2);
2179
					},
2180
				),
2181
				't' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => '  ', 'validate' => function ($tongue) use ($smcFunc)
2182
					{
2183
						return $smcFunc['substr']($tongue . '  ', 0, 2);
2184
					},
2185
				),
2186
			),
2187
			'before' => '<pre data-e="{e}" data-t="{t}"><div>',
2188
			'after' => '</div><script>' . '$("head").append("<style>" + ' . JavaScriptEscape(base64_decode('cHJlW2RhdGEtZV1bZGF0YS10XXt3aGl0ZS1zcGFjZTpwcmUtd3JhcDtsaW5lLWhlaWdodDppbml0aWFsO31wcmVbZGF0YS1lXVtkYXRhLXRdID4gZGl2e2Rpc3BsYXk6dGFibGU7Ym9yZGVyOjFweCBzb2xpZDtib3JkZXItcmFkaXVzOjAuNWVtO3BhZGRpbmc6MWNoO21heC13aWR0aDo4MGNoO21pbi13aWR0aDoxMmNoO31wcmVbZGF0YS1lXVtkYXRhLXRdOjphZnRlcntkaXNwbGF5OmlubGluZS1ibG9jazttYXJnaW4tbGVmdDo4Y2g7bWluLXdpZHRoOjIwY2g7ZGlyZWN0aW9uOmx0cjtjb250ZW50OidcNUMgJycgJycgXl9fXlxBICcnIFw1QyAnJyAoJyBhdHRyKGRhdGEtZSkgJylcNUNfX19fX19fXEEgJycgJycgJycgKF9fKVw1QyAnJyAnJyAnJyAnJyAnJyAnJyAnJyApXDVDL1w1Q1xBICcnICcnICcnICcnICcgYXR0cihkYXRhLXQpICcgfHwtLS0tdyB8XEEgJycgJycgJycgJycgJycgJycgJycgfHwgJycgJycgJycgJycgfHwnO30=')) . ' + "</style>");' . '</script></pre>',
2189
			'block_level' => true,
2190
		);
2191
2192
		foreach ($codes as $code)
2193
		{
2194
			// Make it easier to process parameters later
2195
			if (!empty($code['parameters']))
2196
				ksort($code['parameters'], SORT_STRING);
2197
2198
			// If we are not doing every tag only do ones we are interested in.
2199
			if (empty($parse_tags) || in_array($code['tag'], $parse_tags))
2200
				$bbc_codes[substr($code['tag'], 0, 1)][] = $code;
2201
		}
2202
		$codes = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $codes is dead and can be removed.
Loading history...
2203
	}
2204
2205
	// Shall we take the time to cache this?
2206
	if ($cache_id != '' && !empty($cache_enable) && (($cache_enable >= 2 && isset($message[1000])) || isset($message[2400])) && empty($parse_tags))
2207
	{
2208
		// It's likely this will change if the message is modified.
2209
		$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']);
2210
2211
		if (($temp = cache_get_data($cache_key, 240)) != null)
2212
			return $temp;
2213
2214
		$cache_t = microtime(true);
2215
	}
2216
2217
	if ($smileys === 'print')
0 ignored issues
show
introduced by
The condition $smileys === 'print' is always false.
Loading history...
2218
	{
2219
		// [glow], [shadow], and [move] can't really be printed.
2220
		$disabled['glow'] = true;
2221
		$disabled['shadow'] = true;
2222
		$disabled['move'] = true;
2223
2224
		// Colors can't well be displayed... supposed to be black and white.
2225
		$disabled['color'] = true;
2226
		$disabled['black'] = true;
2227
		$disabled['blue'] = true;
2228
		$disabled['white'] = true;
2229
		$disabled['red'] = true;
2230
		$disabled['green'] = true;
2231
		$disabled['me'] = true;
2232
2233
		// Color coding doesn't make sense.
2234
		$disabled['php'] = true;
2235
2236
		// Links are useless on paper... just show the link.
2237
		$disabled['ftp'] = true;
2238
		$disabled['url'] = true;
2239
		$disabled['iurl'] = true;
2240
		$disabled['email'] = true;
2241
		$disabled['flash'] = true;
2242
2243
		// @todo Change maybe?
2244
		if (!isset($_GET['images']))
2245
		{
2246
			$disabled['img'] = true;
2247
			$disabled['attach'] = true;
2248
		}
2249
2250
		// Maybe some custom BBC need to be disabled for printing.
2251
		call_integration_hook('integrate_bbc_print', array(&$disabled));
2252
	}
2253
2254
	$open_tags = array();
2255
	$message = strtr($message, array("\n" => '<br>'));
2256
2257
	if (!empty($parse_tags))
2258
	{
2259
		$real_alltags_regex = $alltags_regex;
2260
		$alltags_regex = '';
2261
	}
2262
	if (empty($alltags_regex))
2263
	{
2264
		$alltags = array();
2265
		foreach ($bbc_codes as $section)
2266
		{
2267
			foreach ($section as $code)
2268
				$alltags[] = $code['tag'];
2269
		}
2270
		$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

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

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

2501
							$pcre_subroutines['bracket_quote'] = '[' . $bracket_quote_chars . ']|&' . /** @scrutinizer ignore-type */ build_regex($bracket_quote_entities, '~');
Loading history...
2502
							$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

2502
							$pcre_subroutines['allowed_entities'] = '&(?!' . /** @scrutinizer ignore-type */ build_regex(array_merge($bracket_quote_entities, array('lt;', 'gt;')), '~') . ')';
Loading history...
2503
							$pcre_subroutines['excluded_lookahead'] = '(?![' . $excluded_trailing_chars . ']*(?>' . $space_chars . '|<br>|$))';
2504
2505
							foreach (array('path', 'query', 'fragment') as $part)
2506
							{
2507
								switch ($part) {
2508
									case 'path':
2509
										$part_disallowed_chars = '<>' . $bracket_quote_chars . $space_chars . $excluded_trailing_chars . '/#&';
2510
										$part_excluded_trailing_chars = str_replace('?', '', $excluded_trailing_chars);
2511
										break;
2512
2513
									case 'query':
2514
										$part_disallowed_chars = '<>' . $bracket_quote_chars . $space_chars . $excluded_trailing_chars . '#&';
2515
										$part_excluded_trailing_chars = $excluded_trailing_chars;
2516
										break;
2517
2518
									default:
2519
										$part_disallowed_chars = '<>' . $bracket_quote_chars . $space_chars . $excluded_trailing_chars . '&';
2520
										$part_excluded_trailing_chars = $excluded_trailing_chars;
2521
										break;
2522
								}
2523
								$pcre_subroutines[$part . '_allowed'] = '[^' . $part_disallowed_chars . ']|(?P>allowed_entities)|[' . $part_excluded_trailing_chars . '](?P>excluded_lookahead)';
2524
2525
								$balanced_construct_regex = array();
2526
2527
								foreach ($balanced_pairs as $pair_opener => $pair_closer)
2528
									$balanced_construct_regex[] = preg_quote($pair_opener) . '(?P>' . $part . '_recursive)*+' . preg_quote($pair_closer);
2529
2530
								$pcre_subroutines[$part . '_balanced'] = '(?:' . implode('|', $balanced_construct_regex) . ')(?P>' . $part . '_allowed)*+';
2531
								$pcre_subroutines[$part . '_recursive'] = '(?' . '>(?P>' . $part . '_allowed)|(?P>' . $part . '_balanced))';
2532
2533
								$pcre_subroutines[$part . '_segment'] =
2534
									// Allowed characters besides brackets and quotation marks
2535
									'(?P>' . $part . '_allowed)*+' .
2536
									// Brackets and quotation marks that are either...
2537
									'(?:' .
2538
										// part of a balanced construct
2539
										'(?P>' . $part . '_balanced)' .
2540
										// or
2541
										'|' .
2542
										// unpaired but not at the end
2543
										'(?P>bracket_quote)(?=(?P>' . $part . '_allowed))' .
2544
									')*+';
2545
							}
2546
2547
							// Time to build this monster!
2548
							// First, define the PCRE subroutines.
2549
							$url_regex = '(?(DEFINE)';
2550
2551
							foreach ($pcre_subroutines as $name => $subroutine)
2552
								$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

2552
								$url_regex .= '(?<' . $name . '>' . /** @scrutinizer ignore-type */ $subroutine . ')';
Loading history...
2553
2554
							$url_regex .= ')';
2555
2556
							// Now build the rest of the regex
2557
							$url_regex .=
2558
							// 1. IRI scheme and domain components
2559
							'(?:' .
2560
								// 1a. IRIs with a scheme, or at least an opening "//"
2561
								'(?:' .
2562
2563
									// URI scheme (or lack thereof for schemeless URLs)
2564
									'(?:' .
2565
										// URI scheme and colon
2566
										'\b' .
2567
										'(?:' .
2568
											// Either a scheme that need a domain in the authority
2569
											// (Remember for later that we need a domain)
2570
											'(?P<need_domain>(?P>scheme_need_domain)):' .
2571
											// or
2572
											'|' .
2573
											// a scheme that allows an empty authority
2574
											// (Remember for later that the authority can be empty)
2575
											'(?P<empty_authority>(?P>scheme_empty_authority)):' .
2576
											// or
2577
											'|' .
2578
											// a scheme that uses no authority
2579
											'(?P>scheme_no_authority):(?!//)' .
2580
											// or
2581
											'|' .
2582
											// another scheme, but only if it is followed by "://"
2583
											'(?P>scheme_any):(?=//)' .
2584
										')' .
2585
2586
										// or
2587
										'|' .
2588
2589
										// An empty string followed by "//" for schemeless URLs
2590
										'(?P<schemeless>(?=//))' .
2591
									')' .
2592
2593
									// IRI authority chunk (maybe)
2594
									'(?:' .
2595
										// (Keep track of whether we find a valid authority or not)
2596
										'(?P<has_authority>' .
2597
											// 2 slashes before the authority itself
2598
											'//' .
2599
											'(?:' .
2600
												// If there was no scheme...
2601
												'(?(<schemeless>)' .
2602
													// require an authority that contains a domain.
2603
													'(?P>authority)' .
2604
2605
													// Else if a domain is needed...
2606
													'|(?(<need_domain>)' .
2607
														// require an authority with a domain.
2608
														'(?P>authority)' .
2609
2610
														// Else if an empty authority is allowed...
2611
														'|(?(<empty_authority>)' .
2612
															// then require either
2613
															'(?:' .
2614
																// empty string, followed by a "/"
2615
																'(?=/)' .
2616
																// or
2617
																'|' .
2618
																// an authority with a domain.
2619
																'(?P>authority)' .
2620
															')' .
2621
2622
															// Else just a run of IRI characters.
2623
															'|(?P>no_domain)' .
2624
														')' .
2625
													')' .
2626
												')' .
2627
											')' .
2628
											// Followed by a non-domain character or end of line
2629
											'(?=(?P>not_domain_label_char)|$)' .
2630
										')' .
2631
2632
										// or, if there is a scheme but no authority
2633
										// (e.g. "mailto:" URLs)...
2634
										'|' .
2635
2636
										// A run of IRI characters
2637
										'(?P>no_domain)' .
2638
										// If scheme needs a domain, require a dot and a TLD
2639
										'(?(<need_domain>)\.(?P>tlds))' .
2640
										// Followed by a non-domain character or end of line
2641
										'(?=(?P>not_domain_label_char)|$)' .
2642
									')' .
2643
								')' .
2644
2645
								// Or, if there is neither a scheme nor an authority...
2646
								'|' .
2647
2648
								// 1b. Naked domains
2649
								// (e.g. "example.com" in "Go to example.com for an example.")
2650
								'(?P<naked_domain>' .
2651
									// Preceded by start of line or a space
2652
									'(?<=^|<br>|[' . $space_chars . '])' .
2653
									// A domain name
2654
									'(?P>domain)' .
2655
									// Followed by a non-domain character or end of line
2656
									'(?=(?P>not_domain_label_char)|$)' .
2657
								')' .
2658
							')' .
2659
2660
							// 2. IRI path, query, and fragment components (if present)
2661
							'(?:' .
2662
								// If the IRI has an authority or is a naked domain and any of these
2663
								// components exist, the path must start with a single "/".
2664
								// Note: technically, it is valid to append a query or fragment
2665
								// directly to the authority chunk without a "/", but supporting
2666
								// that in the autolinker would produce a lot of false positives,
2667
								// so we don't.
2668
								'(?=' .
2669
									// If we found an authority above...
2670
									'(?(<has_authority>)' .
2671
										// require a "/"
2672
										'/' .
2673
										// Else if we found a naked domain above...
2674
										'|(?(<naked_domain>)' .
2675
											// require a "/"
2676
											'/' .
2677
										')' .
2678
									')' .
2679
								')' .
2680
2681
								// 2.a. Path component, if any.
2682
								'(?:' .
2683
									// Can have one or more segments
2684
									'(?:' .
2685
										// Not preceded by a "/", except in the special case of an
2686
										// empty authority immediately before the path.
2687
										'(?(<empty_authority>)' .
2688
											'(?:(?<=://)|(?<!/))' .
2689
											'|' .
2690
											'(?<!/)' .
2691
										')' .
2692
										// Initial "/"
2693
										'/' .
2694
										// Then a run of allowed path segement characters
2695
										'(?P>path_segment)*+' .
2696
									')*+' .
2697
								')' .
2698
2699
								// 2.b. Query component, if any.
2700
								'(?:' .
2701
									// Initial "?" that is not last character.
2702
									'\?' . '(?=(?P>bracket_quote)*(?P>query_allowed))' .
2703
									// Then a run of allowed query characters
2704
									'(?P>query_segment)*+' .
2705
								')?' .
2706
2707
								// 2.c. Fragment component, if any.
2708
								'(?:' .
2709
									// Initial "#" that is not last character.
2710
									'#' . '(?=(?P>bracket_quote)*(?P>fragment_allowed))' .
2711
									// Then a run of allowed fragment characters
2712
									'(?P>fragment_segment)*+' .
2713
								')?' .
2714
							')?+';
2715
						}
2716
2717
						$tmp_data = preg_replace_callback('~' . $url_regex . '~i' . ($context['utf8'] ? 'u' : ''), function($matches) use ($schemes)
2718
						{
2719
							$url = array_shift($matches);
2720
2721
							// If this isn't a clean URL, bail out
2722
							if ($url != sanitize_iri($url))
2723
								return $url;
2724
2725
							$parsedurl = parse_url($url);
2726
2727
							if (!isset($parsedurl['scheme']))
2728
								$parsedurl['scheme'] = '';
2729
2730
							if ($parsedurl['scheme'] == 'mailto')
2731
							{
2732
								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...
2733
									return $url;
2734
2735
								// Is this version of PHP capable of validating this email address?
2736
								$can_validate = defined('FILTER_FLAG_EMAIL_UNICODE') || strlen($parsedurl['path']) == strspn(strtolower($parsedurl['path']), 'abcdefghijklmnopqrstuvwxyz0123456789!#$%&\'*+-/=?^_`{|}~.@');
2737
2738
								$flags = defined('FILTER_FLAG_EMAIL_UNICODE') ? FILTER_FLAG_EMAIL_UNICODE : null;
2739
2740
								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

2740
								if (!$can_validate || filter_var($parsedurl['path'], FILTER_VALIDATE_EMAIL, /** @scrutinizer ignore-type */ $flags) !== false)
Loading history...
2741
									return '[email=' . str_replace('mailto:', '', $url) . ']' . $url . '[/email]';
2742
								else
2743
									return $url;
2744
							}
2745
2746
							// Are we linking a schemeless URL or naked domain name (e.g. "example.com")?
2747
							if (empty($parsedurl['scheme']))
2748
								$fullUrl = '//' . ltrim($url, ':/');
2749
							else
2750
								$fullUrl = $url;
2751
2752
							// Make sure that $fullUrl really is valid
2753
							if (!in_array($parsedurl['scheme'], $schemes['no_authority']) && validate_iri((strpos($fullUrl, '//') === 0 ? 'http:' : '') . $fullUrl) === false)
2754
								return $url;
2755
2756
							return '[url=&quot;' . str_replace(array('[', ']'), array('&#91;', '&#93;'), $fullUrl) . '&quot;]' . $url . '[/url]';
2757
						}, $data);
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)
2822
			{
2823
				return $code['tag'];
2824
			}, $open_tags)))
2825
				continue;
2826
2827
			$to_close = array();
2828
			$block_level = null;
2829
2830
			do
2831
			{
2832
				$tag = array_pop($open_tags);
2833
				if (!$tag)
2834
					break;
2835
2836
				if (!empty($tag['block_level']))
2837
				{
2838
					// Only find out if we need to.
2839
					if ($block_level === false)
2840
					{
2841
						array_push($open_tags, $tag);
2842
						break;
2843
					}
2844
2845
					// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
2846
					if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]]))
2847
					{
2848
						foreach ($bbc_codes[$look_for[0]] as $temp)
2849
							if ($temp['tag'] == $look_for)
2850
							{
2851
								$block_level = !empty($temp['block_level']);
2852
								break;
2853
							}
2854
					}
2855
2856
					if ($block_level !== true)
2857
					{
2858
						$block_level = false;
2859
						array_push($open_tags, $tag);
2860
						break;
2861
					}
2862
				}
2863
2864
				$to_close[] = $tag;
2865
			}
2866
			while ($tag['tag'] != $look_for);
2867
2868
			// Did we just eat through everything and not find it?
2869
			if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
2870
			{
2871
				$open_tags = $to_close;
2872
				continue;
2873
			}
2874
			elseif (!empty($to_close) && $tag['tag'] != $look_for)
2875
			{
2876
				if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]]))
2877
				{
2878
					foreach ($bbc_codes[$look_for[0]] as $temp)
2879
						if ($temp['tag'] == $look_for)
2880
						{
2881
							$block_level = !empty($temp['block_level']);
2882
							break;
2883
						}
2884
				}
2885
2886
				// We're not looking for a block level tag (or maybe even a tag that exists...)
2887
				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...
2888
				{
2889
					foreach ($to_close as $tag)
2890
						array_push($open_tags, $tag);
2891
					continue;
2892
				}
2893
			}
2894
2895
			foreach ($to_close as $tag)
2896
			{
2897
				$message = substr($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1);
2898
				$pos += strlen($tag['after']) + 2;
2899
				$pos2 = $pos - 1;
2900
2901
				// See the comment at the end of the big loop - just eating whitespace ;).
2902
				$whitespace_regex = '';
2903
				if (!empty($tag['block_level']))
2904
					$whitespace_regex .= '(&nbsp;|\s)*(<br\s*/?' . '>)?';
2905
				// Trim one line of whitespace after unnested tags, but all of it after nested ones
2906
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
2907
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2908
2909
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
2910
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2911
			}
2912
2913
			if (!empty($to_close))
2914
			{
2915
				$to_close = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $to_close is dead and can be removed.
Loading history...
2916
				$pos--;
2917
			}
2918
2919
			continue;
2920
		}
2921
2922
		// No tags for this character, so just keep going (fastest possible course.)
2923
		if (!isset($bbc_codes[$tag_character]))
2924
			continue;
2925
2926
		$inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
2927
		$tag = null;
2928
		foreach ($bbc_codes[$tag_character] as $possible)
2929
		{
2930
			$pt_strlen = strlen($possible['tag']);
2931
2932
			// Not a match?
2933
			if (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag'])
2934
				continue;
2935
2936
			$next_c = isset($message[$pos + 1 + $pt_strlen]) ? $message[$pos + 1 + $pt_strlen] : '';
2937
2938
			// A tag is the last char maybe
2939
			if ($next_c == '')
2940
				break;
2941
2942
			// A test validation?
2943
			if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0)
2944
				continue;
2945
			// Do we want parameters?
2946
			elseif (!empty($possible['parameters']))
2947
			{
2948
				// Are all the parameters optional?
2949
				$param_required = false;
2950
				foreach ($possible['parameters'] as $param)
2951
				{
2952
					if (empty($param['optional']))
2953
					{
2954
						$param_required = true;
2955
						break;
2956
					}
2957
				}
2958
2959
				if ($param_required && $next_c != ' ')
2960
					continue;
2961
			}
2962
			elseif (isset($possible['type']))
2963
			{
2964
				// Do we need an equal sign?
2965
				if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=')
2966
					continue;
2967
				// Maybe we just want a /...
2968
				if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + $pt_strlen, 2) != '/]' && substr($message, $pos + 1 + $pt_strlen, 3) != ' /]')
2969
					continue;
2970
				// An immediate ]?
2971
				if ($possible['type'] == 'unparsed_content' && $next_c != ']')
2972
					continue;
2973
			}
2974
			// No type means 'parsed_content', which demands an immediate ] without parameters!
2975
			elseif ($next_c != ']')
2976
				continue;
2977
2978
			// Check allowed tree?
2979
			if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
2980
				continue;
2981
			elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
2982
				continue;
2983
			// If this is in the list of disallowed child tags, don't parse it.
2984
			elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
2985
				continue;
2986
2987
			$pos1 = $pos + 1 + $pt_strlen + 1;
2988
2989
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
2990
			if ($possible['tag'] == 'quote')
2991
			{
2992
				// Start with standard
2993
				$quote_alt = false;
2994
				foreach ($open_tags as $open_quote)
2995
				{
2996
					// Every parent quote this quote has flips the styling
2997
					if ($open_quote['tag'] == 'quote')
2998
						$quote_alt = !$quote_alt;
0 ignored issues
show
introduced by
The condition $quote_alt is always false.
Loading history...
2999
				}
3000
				// Add a class to the quote to style alternating blockquotes
3001
				$possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">'));
3002
			}
3003
3004
			// This is long, but it makes things much easier and cleaner.
3005
			if (!empty($possible['parameters']))
3006
			{
3007
				// Build a regular expression for each parameter for the current tag.
3008
				$regex_key = $smcFunc['json_encode']($possible['parameters']);
3009
				if (!isset($params_regexes[$regex_key]))
3010
				{
3011
					$params_regexes[$regex_key] = '';
3012
3013
					foreach ($possible['parameters'] as $p => $info)
3014
						$params_regexes[$regex_key] .= '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . '\s*)' . (empty($info['optional']) ? '' : '?');
3015
				}
3016
3017
				// Extract the string that potentially holds our parameters.
3018
				$blob = preg_split('~\[/?(?:' . $alltags_regex . ')~i', substr($message, $pos));
3019
				$blobs = preg_split('~\]~i', $blob[1]);
3020
3021
				$splitters = implode('=|', array_keys($possible['parameters'])) . '=';
3022
3023
				// Progressively append more blobs until we find our parameters or run out of blobs
3024
				$blob_counter = 1;
3025
				while ($blob_counter <= count($blobs))
3026
				{
3027
					$given_param_string = implode(']', array_slice($blobs, 0, $blob_counter++));
3028
3029
					$given_params = preg_split('~\s(?=(' . $splitters . '))~i', $given_param_string);
3030
					sort($given_params, SORT_STRING);
3031
3032
					$match = preg_match('~^' . $params_regexes[$regex_key] . '$~i', implode(' ', $given_params), $matches) !== 0;
3033
3034
					if ($match)
3035
						break;
3036
				}
3037
3038
				// Didn't match our parameter list, try the next possible.
3039
				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...
3040
					continue;
3041
3042
				$params = array();
3043
				for ($i = 1, $n = count($matches); $i < $n; $i += 2)
3044
				{
3045
					$key = strtok(ltrim($matches[$i]), '=');
3046
					if ($key === false)
3047
						continue;
3048
					elseif (isset($possible['parameters'][$key]['value']))
3049
						$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
3050
					elseif (isset($possible['parameters'][$key]['validate']))
3051
						$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
3052
					else
3053
						$params['{' . $key . '}'] = $matches[$i + 1];
3054
3055
					// Just to make sure: replace any $ or { so they can't interpolate wrongly.
3056
					$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
3057
				}
3058
3059
				foreach ($possible['parameters'] as $p => $info)
3060
				{
3061
					if (!isset($params['{' . $p . '}']))
3062
					{
3063
						if (!isset($info['default']))
3064
							$params['{' . $p . '}'] = '';
3065
						elseif (isset($possible['parameters'][$p]['value']))
3066
							$params['{' . $p . '}'] = strtr($possible['parameters'][$p]['value'], array('$1' => $info['default']));
3067
						elseif (isset($possible['parameters'][$p]['validate']))
3068
							$params['{' . $p . '}'] = $possible['parameters'][$p]['validate']($info['default']);
3069
						else
3070
							$params['{' . $p . '}'] = $info['default'];
3071
					}
3072
				}
3073
3074
				$tag = $possible;
3075
3076
				// Put the parameters into the string.
3077
				if (isset($tag['before']))
3078
					$tag['before'] = strtr($tag['before'], $params);
3079
				if (isset($tag['after']))
3080
					$tag['after'] = strtr($tag['after'], $params);
3081
				if (isset($tag['content']))
3082
					$tag['content'] = strtr($tag['content'], $params);
3083
3084
				$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...
3085
			}
3086
			else
3087
			{
3088
				$tag = $possible;
3089
				$params = array();
3090
			}
3091
			break;
3092
		}
3093
3094
		// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
3095
		if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
3096
		{
3097
			if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
3098
				continue;
3099
3100
			$tag = $itemcodes[$message[$pos + 1]];
3101
3102
			// First let's set up the tree: it needs to be in a list, or after an li.
3103
			if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
3104
			{
3105
				$open_tags[] = array(
3106
					'tag' => 'list',
3107
					'after' => '</ul>',
3108
					'block_level' => true,
3109
					'require_children' => array('li'),
3110
					'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
3111
				);
3112
				$code = '<ul class="bbc_list">';
3113
			}
3114
			// We're in a list item already: another itemcode?  Close it first.
3115
			elseif ($inside['tag'] == 'li')
3116
			{
3117
				array_pop($open_tags);
3118
				$code = '</li>';
3119
			}
3120
			else
3121
				$code = '';
3122
3123
			// Now we open a new tag.
3124
			$open_tags[] = array(
3125
				'tag' => 'li',
3126
				'after' => '</li>',
3127
				'trim' => 'outside',
3128
				'block_level' => true,
3129
				'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
3130
			);
3131
3132
			// First, open the tag...
3133
			$code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
3134
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
3135
			$pos += strlen($code) - 1 + 2;
3136
3137
			// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
3138
			$pos2 = strpos($message, '<br>', $pos);
3139
			$pos3 = strpos($message, '[/', $pos);
3140
			if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
3141
			{
3142
				preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 4), $matches);
3143
				$message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
3144
3145
				$open_tags[count($open_tags) - 2]['after'] = '</ul>';
3146
			}
3147
			// Tell the [list] that it needs to close specially.
3148
			else
3149
			{
3150
				// Move the li over, because we're not sure what we'll hit.
3151
				$open_tags[count($open_tags) - 1]['after'] = '';
3152
				$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
3153
			}
3154
3155
			continue;
3156
		}
3157
3158
		// Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
3159
		if ($tag === null && $inside !== null && !empty($inside['require_children']))
3160
		{
3161
			array_pop($open_tags);
3162
3163
			$message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
3164
			$pos += strlen($inside['after']) - 1 + 2;
3165
		}
3166
3167
		// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
3168
		if ($tag === null)
3169
			continue;
3170
3171
		// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
3172
		if (isset($inside['disallow_children']))
3173
			$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
3174
3175
		// Is this tag disabled?
3176
		if (isset($disabled[$tag['tag']]))
3177
		{
3178
			if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
3179
			{
3180
				$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
3181
				$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
3182
				$tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
3183
			}
3184
			elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
3185
			{
3186
				$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
3187
				$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
3188
			}
3189
			else
3190
				$tag['content'] = $tag['disabled_content'];
3191
		}
3192
3193
		// we use this a lot
3194
		$tag_strlen = strlen($tag['tag']);
3195
3196
		// The only special case is 'html', which doesn't need to close things.
3197
		if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
3198
		{
3199
			$n = count($open_tags) - 1;
3200
			while (empty($open_tags[$n]['block_level']) && $n >= 0)
3201
				$n--;
3202
3203
			// Close all the non block level tags so this tag isn't surrounded by them.
3204
			for ($i = count($open_tags) - 1; $i > $n; $i--)
3205
			{
3206
				$message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
3207
				$ot_strlen = strlen($open_tags[$i]['after']);
3208
				$pos += $ot_strlen + 2;
3209
				$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...
3210
3211
				// Trim or eat trailing stuff... see comment at the end of the big loop.
3212
				$whitespace_regex = '';
3213
				if (!empty($tag['block_level']))
3214
					$whitespace_regex .= '(&nbsp;|\s)*(<br>)?';
3215
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
3216
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
3217
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
3218
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
3219
3220
				array_pop($open_tags);
3221
			}
3222
		}
3223
3224
		// Can't read past the end of the message
3225
		$pos1 = min(strlen($message), $pos1);
3226
3227
		// No type means 'parsed_content'.
3228
		if (!isset($tag['type']))
3229
		{
3230
			$open_tags[] = $tag;
3231
			$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
3232
			$pos += strlen($tag['before']) - 1 + 2;
3233
		}
3234
		// Don't parse the content, just skip it.
3235
		elseif ($tag['type'] == 'unparsed_content')
3236
		{
3237
			$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
3238
			if ($pos2 === false)
3239
				continue;
3240
3241
			$data = substr($message, $pos1, $pos2 - $pos1);
3242
3243
			if (!empty($tag['block_level']) && substr($data, 0, 4) == '<br>')
3244
				$data = substr($data, 4);
3245
3246
			if (isset($tag['validate']))
3247
				$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...
3248
3249
			$code = strtr($tag['content'], array('$1' => $data));
3250
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
3251
3252
			$pos += strlen($code) - 1 + 2;
3253
			$last_pos = $pos + 1;
3254
		}
3255
		// Don't parse the content, just skip it.
3256
		elseif ($tag['type'] == 'unparsed_equals_content')
3257
		{
3258
			// The value may be quoted for some tags - check.
3259
			if (isset($tag['quoted']))
3260
			{
3261
				$quoted = substr($message, $pos1, 6) == '&quot;';
3262
				if ($tag['quoted'] != 'optional' && !$quoted)
3263
					continue;
3264
3265
				if ($quoted)
3266
					$pos1 += 6;
3267
			}
3268
			else
3269
				$quoted = false;
3270
3271
			$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...
3272
			if ($pos2 === false)
3273
				continue;
3274
3275
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3276
			if ($pos3 === false)
3277
				continue;
3278
3279
			$data = array(
3280
				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...
3281
				substr($message, $pos1, $pos2 - $pos1)
3282
			);
3283
3284
			if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '<br>')
3285
				$data[0] = substr($data[0], 4);
3286
3287
			// Validation for my parking, please!
3288
			if (isset($tag['validate']))
3289
				$tag['validate']($tag, $data, $disabled, $params);
3290
3291
			$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
3292
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3293
			$pos += strlen($code) - 1 + 2;
3294
		}
3295
		// A closed tag, with no content or value.
3296
		elseif ($tag['type'] == 'closed')
3297
		{
3298
			$pos2 = strpos($message, ']', $pos);
3299
			$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
3300
			$pos += strlen($tag['content']) - 1 + 2;
3301
		}
3302
		// This one is sorta ugly... :/.  Unfortunately, it's needed for flash.
3303
		elseif ($tag['type'] == 'unparsed_commas_content')
3304
		{
3305
			$pos2 = strpos($message, ']', $pos1);
3306
			if ($pos2 === false)
3307
				continue;
3308
3309
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3310
			if ($pos3 === false)
3311
				continue;
3312
3313
			// We want $1 to be the content, and the rest to be csv.
3314
			$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
3315
			$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
3316
3317
			if (isset($tag['validate']))
3318
				$tag['validate']($tag, $data, $disabled, $params);
3319
3320
			$code = $tag['content'];
3321
			foreach ($data as $k => $d)
3322
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3323
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3324
			$pos += strlen($code) - 1 + 2;
3325
		}
3326
		// This has parsed content, and a csv value which is unparsed.
3327
		elseif ($tag['type'] == 'unparsed_commas')
3328
		{
3329
			$pos2 = strpos($message, ']', $pos1);
3330
			if ($pos2 === false)
3331
				continue;
3332
3333
			$data = explode(',', substr($message, $pos1, $pos2 - $pos1));
3334
3335
			if (isset($tag['validate']))
3336
				$tag['validate']($tag, $data, $disabled, $params);
3337
3338
			// Fix after, for disabled code mainly.
3339
			foreach ($data as $k => $d)
3340
				$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
3341
3342
			$open_tags[] = $tag;
3343
3344
			// Replace them out, $1, $2, $3, $4, etc.
3345
			$code = $tag['before'];
3346
			foreach ($data as $k => $d)
3347
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3348
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1);
3349
			$pos += strlen($code) - 1 + 2;
3350
		}
3351
		// A tag set to a value, parsed or not.
3352
		elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
3353
		{
3354
			// The value may be quoted for some tags - check.
3355
			if (isset($tag['quoted']))
3356
			{
3357
				$quoted = substr($message, $pos1, 6) == '&quot;';
3358
				if ($tag['quoted'] != 'optional' && !$quoted)
3359
					continue;
3360
3361
				if ($quoted)
3362
					$pos1 += 6;
3363
			}
3364
			else
3365
				$quoted = false;
3366
3367
			if ($quoted)
3368
			{
3369
				$end_of_value = strpos($message, '&quot;]', $pos1);
3370
				$nested_tag = strpos($message, '=&quot;', $pos1);
3371
				if ($nested_tag && $nested_tag < $end_of_value)
3372
					// Nested tag with quoted value detected, use next end tag
3373
					$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...
3374
			}
3375
3376
			$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...
3377
			if ($pos2 === false)
3378
				continue;
3379
3380
			$data = substr($message, $pos1, $pos2 - $pos1);
3381
3382
			// Validation for my parking, please!
3383
			if (isset($tag['validate']))
3384
				$tag['validate']($tag, $data, $disabled, $params);
3385
3386
			// For parsed content, we must recurse to avoid security problems.
3387
			if ($tag['type'] != 'unparsed_equals')
3388
				$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
3389
3390
			$tag['after'] = strtr($tag['after'], array('$1' => $data));
3391
3392
			$open_tags[] = $tag;
3393
3394
			$code = strtr($tag['before'], array('$1' => $data));
3395
			$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...
3396
			$pos += strlen($code) - 1 + 2;
3397
		}
3398
3399
		// If this is block level, eat any breaks after it.
3400
		if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '<br>')
3401
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 5);
3402
3403
		// Are we trimming outside this tag?
3404
		if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
3405
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
3406
	}
3407
3408
	// Close any remaining tags.
3409
	while ($tag = array_pop($open_tags))
3410
		$message .= "\n" . $tag['after'] . "\n";
3411
3412
	// Parse the smileys within the parts where it can be done safely.
3413
	if ($smileys === true)
3414
	{
3415
		$message_parts = explode("\n", $message);
3416
		for ($i = 0, $n = count($message_parts); $i < $n; $i += 2)
3417
			parsesmileys($message_parts[$i]);
3418
3419
		$message = implode('', $message_parts);
3420
	}
3421
3422
	// No smileys, just get rid of the markers.
3423
	else
3424
		$message = strtr($message, array("\n" => ''));
3425
3426
	if ($message !== '' && $message[0] === ' ')
3427
		$message = '&nbsp;' . substr($message, 1);
3428
3429
	// Cleanup whitespace.
3430
	$message = strtr($message, array('  ' => ' &nbsp;', "\r" => '', "\n" => '<br>', '<br> ' => '<br>&nbsp;', '&#13;' => "\n"));
3431
3432
	// Allow mods access to what parse_bbc created
3433
	call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
3434
3435
	// Cache the output if it took some time...
3436
	if (isset($cache_key, $cache_t) && microtime(true) - $cache_t > 0.05)
3437
		cache_put_data($cache_key, $message, 240);
3438
3439
	// If this was a force parse revert if needed.
3440
	if (!empty($parse_tags))
3441
	{
3442
		$alltags_regex = empty($real_alltags_regex) ? '' : $real_alltags_regex;
3443
		unset($real_alltags_regex);
3444
	}
3445
	elseif (!empty($bbc_codes))
3446
		$bbc_lang_locales[$txt['lang_locale']] = $bbc_codes;
3447
3448
	return $message;
3449
}
3450
3451
/**
3452
 * Parse smileys in the passed message.
3453
 *
3454
 * The smiley parsing function which makes pretty faces appear :).
3455
 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
3456
 * These are specifically not parsed in code tags [url=mailto:[email protected]]
3457
 * Caches the smileys from the database or array in memory.
3458
 * Doesn't return anything, but rather modifies message directly.
3459
 *
3460
 * @param string &$message The message to parse smileys in
3461
 */
3462
function parsesmileys(&$message)
3463
{
3464
	global $modSettings, $txt, $user_info, $context, $smcFunc;
3465
	static $smileyPregSearch = null, $smileyPregReplacements = array();
3466
3467
	// No smiley set at all?!
3468
	if ($user_info['smiley_set'] == 'none' || trim($message) == '')
3469
		return;
3470
3471
	// Maybe a mod wants to implement an alternative method (e.g. emojis instead of images)
3472
	call_integration_hook('integrate_smileys', array(&$smileyPregSearch, &$smileyPregReplacements));
3473
3474
	// If smileyPregSearch hasn't been set, do it now.
3475
	if (empty($smileyPregSearch))
3476
	{
3477
		// Cache for longer when customized smiley codes aren't enabled
3478
		$cache_time = empty($modSettings['smiley_enable']) ? 7200 : 480;
3479
3480
		// Load the smileys in reverse order by length so they don't get parsed incorrectly.
3481
		if (($temp = cache_get_data('parsing_smileys_' . $user_info['smiley_set'], $cache_time)) == null)
3482
		{
3483
			$result = $smcFunc['db_query']('', '
3484
				SELECT s.code, f.filename, s.description
3485
				FROM {db_prefix}smileys AS s
3486
					JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
3487
				WHERE f.smiley_set = {string:smiley_set}' . (empty($modSettings['smiley_enable']) ? '
3488
					AND s.code IN ({array_string:default_codes})' : '') . '
3489
				ORDER BY LENGTH(s.code) DESC',
3490
				array(
3491
					'default_codes' => array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'),
3492
					'smiley_set' => $user_info['smiley_set'],
3493
				)
3494
			);
3495
			$smileysfrom = array();
3496
			$smileysto = array();
3497
			$smileysdescs = array();
3498
			while ($row = $smcFunc['db_fetch_assoc']($result))
3499
			{
3500
				$smileysfrom[] = $row['code'];
3501
				$smileysto[] = $smcFunc['htmlspecialchars']($row['filename']);
3502
				$smileysdescs[] = !empty($txt['icon_' . strtolower($row['description'])]) ? $txt['icon_' . strtolower($row['description'])] : $row['description'];
3503
			}
3504
			$smcFunc['db_free_result']($result);
3505
3506
			cache_put_data('parsing_smileys_' . $user_info['smiley_set'], array($smileysfrom, $smileysto, $smileysdescs), $cache_time);
3507
		}
3508
		else
3509
			list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
3510
3511
		// The non-breaking-space is a complex thing...
3512
		$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
3513
3514
		// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:[email protected]] doesn't parse the :D smiley)
3515
		$smileyPregReplacements = array();
3516
		$searchParts = array();
3517
		$smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
3518
3519
		for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
3520
		{
3521
			$specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES);
3522
			$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">';
3523
3524
			$smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
3525
3526
			$searchParts[] = $smileysfrom[$i];
3527
			if ($smileysfrom[$i] != $specialChars)
3528
			{
3529
				$smileyPregReplacements[$specialChars] = $smileyCode;
3530
				$searchParts[] = $specialChars;
3531
3532
				// Some 2.0 hex htmlchars are in there as 3 digits; allow for finding leading 0 or not
3533
				$specialChars2 = preg_replace('/&#(\d{2});/', '&#0$1;', $specialChars);
3534
				if ($specialChars2 != $specialChars)
3535
				{
3536
					$smileyPregReplacements[$specialChars2] = $smileyCode;
3537
					$searchParts[] = $specialChars2;
3538
				}
3539
			}
3540
		}
3541
3542
		$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

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

3734
					ob_start(/** @scrutinizer ignore-type */ $call);
Loading history...
3735
			}
3736
3737
		// Display the screen in the logical order.
3738
		template_header();
3739
		$header_done = true;
3740
	}
3741
	if ($do_footer)
3742
	{
3743
		loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main');
3744
3745
		// Anything special to put out?
3746
		if (!empty($context['insert_after_template']) && !isset($_REQUEST['xml']))
3747
			echo $context['insert_after_template'];
3748
3749
		// Just so we don't get caught in an endless loop of errors from the footer...
3750
		if (!$footer_done)
3751
		{
3752
			$footer_done = true;
3753
			template_footer();
3754
3755
			// (since this is just debugging... it's okay that it's after </html>.)
3756
			if (!isset($_REQUEST['xml']))
3757
				displayDebug();
3758
		}
3759
	}
3760
3761
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
3762
	if ($should_log)
3763
		$_SESSION['old_url'] = $_SERVER['REQUEST_URL'];
3764
3765
	// For session check verification.... don't switch browsers...
3766
	$_SESSION['USER_AGENT'] = empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT'];
3767
3768
	// Hand off the output to the portal, etc. we're integrated with.
3769
	call_integration_hook('integrate_exit', array($do_footer));
3770
3771
	// Don't exit if we're coming from index.php; that will pass through normally.
3772
	if (!$from_index)
3773
		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...
3774
}
3775
3776
/**
3777
 * Get the size of a specified image with better error handling.
3778
 *
3779
 * @todo see if it's better in Subs-Graphics, but one step at the time.
3780
 * Uses getimagesize() to determine the size of a file.
3781
 * Attempts to connect to the server first so it won't time out.
3782
 *
3783
 * @param string $url The URL of the image
3784
 * @return array|false The image size as array (width, height), or false on failure
3785
 */
3786
function url_image_size($url)
3787
{
3788
	global $sourcedir;
3789
3790
	// Make sure it is a proper URL.
3791
	$url = str_replace(' ', '%20', $url);
3792
3793
	// Can we pull this from the cache... please please?
3794
	if (($temp = cache_get_data('url_image_size-' . md5($url), 240)) !== null)
3795
		return $temp;
3796
	$t = microtime(true);
3797
3798
	// Get the host to pester...
3799
	preg_match('~^\w+://(.+?)/(.*)$~', $url, $match);
3800
3801
	// Can't figure it out, just try the image size.
3802
	if ($url == '' || $url == 'http://' || $url == 'https://')
3803
	{
3804
		return false;
3805
	}
3806
	elseif (!isset($match[1]))
3807
	{
3808
		$size = @getimagesize($url);
3809
	}
3810
	else
3811
	{
3812
		// Try to connect to the server... give it half a second.
3813
		$temp = 0;
3814
		$fp = @fsockopen($match[1], 80, $temp, $temp, 0.5);
3815
3816
		// Successful?  Continue...
3817
		if ($fp != false)
3818
		{
3819
			// Send the HEAD request (since we don't have to worry about chunked, HTTP/1.1 is fine here.)
3820
			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");
3821
3822
			// Read in the HTTP/1.1 or whatever.
3823
			$test = substr(fgets($fp, 11), -1);
3824
			fclose($fp);
3825
3826
			// See if it returned a 404/403 or something.
3827
			if ($test < 4)
3828
			{
3829
				$size = @getimagesize($url);
3830
3831
				// This probably means allow_url_fopen is off, let's try GD.
3832
				if ($size === false && function_exists('imagecreatefromstring'))
3833
				{
3834
					// It's going to hate us for doing this, but another request...
3835
					$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

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

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

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

4841
		if (strpos(/** @scrutinizer ignore-type */ $test, 'not found') !== false)
Loading history...
4842
			$host = '';
4843
		// Invalid server option?
4844
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
4845
			updateSettings(array('host_to_dis' => 1));
4846
		// Maybe it found something, after all?
4847
		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

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

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

5530
	if (strpos(/** @scrutinizer ignore-type */ $string, '::') !== false)
Loading history...
5531
	{
5532
		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

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

5816
			$fetch_data->get_url_data($url, /** @scrutinizer ignore-type */ $post_data);
Loading history...
5817
5818
			// no errors and a 200 result, then we have a good dataset, well we at least have data. ;)
5819
			if ($fetch_data->result('code') == 200 && !$fetch_data->result('error'))
5820
				$data = $fetch_data->result('body');
5821
			else
5822
				return false;
5823
		}
5824
5825
		// Neither fsockopen nor curl are available. Well, phooey.
5826
		else
5827
			return false;
5828
	}
5829
	else
5830
	{
5831
		// Umm, this shouldn't happen?
5832
		loadLanguage('Errors');
5833
		trigger_error($txt['fetch_web_data_bad_url'], E_USER_NOTICE);
5834
		$data = false;
5835
	}
5836
5837
	return $data;
5838
}
5839
5840
/**
5841
 * Attempts to determine the MIME type of some data or a file.
5842
 *
5843
 * @param string $data The data to check, or the path or URL of a file to check.
5844
 * @param string $is_path If true, $data is a path or URL to a file.
5845
 * @return string|bool A MIME type, or false if we cannot determine it.
5846
 */
5847
function get_mime_type($data, $is_path = false)
5848
{
5849
	global $cachedir;
5850
5851
	$finfo_loaded = extension_loaded('fileinfo');
5852
	$exif_loaded = extension_loaded('exif') && function_exists('image_type_to_mime_type');
5853
5854
	// Oh well. We tried.
5855
	if (!$finfo_loaded && !$exif_loaded)
5856
		return false;
5857
5858
	// Start with the 'empty' MIME type.
5859
	$mime_type = 'application/x-empty';
5860
5861
	if ($finfo_loaded)
5862
	{
5863
		// Just some nice, simple data to analyze.
5864
		if (empty($is_path))
5865
			$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
5866
5867
		// A file, or maybe a URL?
5868
		else
5869
		{
5870
			// Local file.
5871
			if (file_exists($data))
5872
				$mime_type = mime_content_type($data);
5873
5874
			// URL.
5875
			elseif ($data = fetch_web_data($data))
5876
				$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
5877
		}
5878
	}
5879
	// Workaround using Exif requires a local file.
5880
	else
5881
	{
5882
		// If $data is a URL to fetch, do so.
5883
		if (!empty($is_path) && !file_exists($data) && url_exists($data))
5884
		{
5885
			$data = fetch_web_data($data);
5886
			$is_path = false;
5887
		}
5888
5889
		// If we don't have a local file, create one and use it.
5890
		if (empty($is_path))
5891
		{
5892
			$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

5892
			$temp_file = tempnam($cachedir, md5(/** @scrutinizer ignore-type */ $data));
Loading history...
5893
			file_put_contents($temp_file, $data);
5894
			$is_path = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $is_path is dead and can be removed.
Loading history...
5895
			$data = $temp_file;
5896
		}
5897
5898
		$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

5898
		$imagetype = @exif_imagetype(/** @scrutinizer ignore-type */ $data);
Loading history...
5899
5900
		if (isset($temp_file))
5901
			unlink($temp_file);
5902
5903
		// Unfortunately, this workaround only works for image files.
5904
		if ($imagetype !== false)
5905
			$mime_type = image_type_to_mime_type($imagetype);
5906
	}
5907
5908
	return $mime_type;
5909
}
5910
5911
/**
5912
 * Checks whether a file or data has the expected MIME type.
5913
 *
5914
 * @param string $data The data to check, or the path or URL of a file to check.
5915
 * @param string $type_pattern A regex pattern to match the acceptable MIME types.
5916
 * @param string $is_path If true, $data is a path or URL to a file.
5917
 * @return int 1 if the detected MIME type matches the pattern, 0 if it doesn't, or 2 if we can't check.
5918
 */
5919
function check_mime_type($data, $type_pattern, $is_path = false)
5920
{
5921
	// Get the MIME type.
5922
	$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

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

6049
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
6050
6051
	// remove left to right / right to left overrides
6052
	if ($num === 0x202D || $num === 0x202E)
6053
		return '';
6054
6055
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
6056
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
6057
		return '&#' . $num . ';';
6058
6059
	if (empty($context['utf8']))
6060
	{
6061
		// no control characters
6062
		if ($num < 0x20)
6063
			return '';
6064
		// text is text
6065
		elseif ($num < 0x80)
6066
			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

6066
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
6067
		// all others get html-ised
6068
		else
6069
			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

6069
			return '&#' . /** @scrutinizer ignore-type */ $matches[2] . ';';
Loading history...
6070
	}
6071
	else
6072
	{
6073
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
6074
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
6075
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
6076
			return '';
6077
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
6078
		elseif ($num < 0x80)
6079
			return chr($num);
6080
		// <0x800 (2048)
6081
		elseif ($num < 0x800)
6082
			return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
6083
		// < 0x10000 (65536)
6084
		elseif ($num < 0x10000)
6085
			return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6086
		// <= 0x10FFFF (1114111)
6087
		else
6088
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6089
	}
6090
}
6091
6092
/**
6093
 * Converts html entities to utf8 equivalents
6094
 *
6095
 * Callback function for preg_replace_callback
6096
 * Uses capture group 1 in the supplied array
6097
 * Does basic checks to keep characters inside a viewable range.
6098
 *
6099
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
6100
 * @return string The fixed string
6101
 */
6102
function fixchar__callback($matches)
6103
{
6104
	if (!isset($matches[1]))
6105
		return '';
6106
6107
	$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

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

6115
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
6116
	// <0x800 (2048)
6117
	elseif ($num < 0x800)
6118
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
6119
	// < 0x10000 (65536)
6120
	elseif ($num < 0x10000)
6121
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6122
	// <= 0x10FFFF (1114111)
6123
	else
6124
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6125
}
6126
6127
/**
6128
 * Strips out invalid html entities, replaces others with html style &#123; codes
6129
 *
6130
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
6131
 * strpos, strlen, substr etc
6132
 *
6133
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
6134
 * @return string The fixed string
6135
 */
6136
function entity_fix__callback($matches)
6137
{
6138
	if (!isset($matches[2]))
6139
		return '';
6140
6141
	$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

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

6245
	$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...
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...
6246
6247
	// Process them in order of importance.
6248
	$tzids = array_merge($priority_tzids, $normal_priority_tzids, $low_priority_tzids);
6249
6250
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
6251
	$dst_types = array();
6252
	$labels = array();
6253
	$offsets = array();
6254
	foreach ($tzids as $tzid)
6255
	{
6256
		// We don't want UTC right now
6257
		if ($tzid == 'UTC')
6258
			continue;
6259
6260
		$tz = @timezone_open($tzid);
6261
6262
		if ($tz == null)
6263
			continue;
6264
6265
		// First, get the set of transition rules for this tzid
6266
		$tzinfo = timezone_transitions_get($tz, $when, $later);
6267
6268
		// Use the entire set of transition rules as the array *key* so we can avoid duplicates
6269
		$tzkey = serialize($tzinfo);
6270
6271
		// ...But make sure to include all explicitly defined meta-zones.
6272
		if (isset($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid]))
6273
			$tzkey = serialize(array_merge($tzinfo, array('metazone' => $tzid_metazones[$tzid])));
6274
6275
		// Don't overwrite our preferred tzids
6276
		if (empty($zones[$tzkey]['tzid']))
6277
		{
6278
			$zones[$tzkey]['tzid'] = $tzid;
6279
			$zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
6280
6281
			foreach ($tzinfo as $transition) {
6282
				$zones[$tzkey]['abbrs'][] = $transition['abbr'];
6283
			}
6284
6285
			if (isset($tzid_metazones[$tzid]))
6286
				$zones[$tzkey]['metazone'] = $tzid_metazones[$tzid];
6287
			else
6288
			{
6289
				$tzgeo = timezone_location_get($tz);
6290
				$country_tzids = get_sorted_tzids_for_country($tzgeo['country_code']);
6291
6292
				if (count($country_tzids) === 1)
6293
					$zones[$tzkey]['metazone'] = $txt['iso3166'][$tzgeo['country_code']];
6294
			}
6295
		}
6296
6297
		// A time zone from a prioritized country?
6298
		if (in_array($tzid, $priority_tzids))
6299
			$priority_zones[$tzkey] = true;
6300
6301
		// Keep track of the location and offset for this tzid
6302
		if (!empty($txt[$tzid]))
6303
			$zones[$tzkey]['locations'][] = $txt[$tzid];
6304
		else
6305
		{
6306
			$tzid_parts = explode('/', $tzid);
6307
			$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
6308
		}
6309
		$offsets[$tzkey] = $tzinfo[0]['offset'];
6310
6311
		// Figure out the "meta-zone" info for the label
6312
		if (empty($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid]))
6313
		{
6314
			$zones[$tzkey]['metazone'] = $tzid_metazones[$tzid];
6315
			$zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
6316
		}
6317
		$dst_types[$tzkey] = count($tzinfo) > 1 ? 'c' : ($tzinfo[0]['isdst'] ? 't' : 'f');
6318
		$labels[$tzkey] = !empty($zones[$tzkey]['metazone']) && !empty($tztxt[$zones[$tzkey]['metazone']]) ? $tztxt[$zones[$tzkey]['metazone']] : '';
6319
6320
		// Remember this for later
6321
		if (isset($cur_profile['timezone']) && $cur_profile['timezone'] == $tzid)
6322
			$member_tzkey = $tzkey;
6323
		if (isset($context['event']['tz']) && $context['event']['tz'] == $tzid)
6324
			$event_tzkey = $tzkey;
6325
	}
6326
6327
	// Sort by offset, then label, then DST type.
6328
	array_multisort($offsets, SORT_ASC, SORT_NUMERIC, $labels, SORT_ASC, $dst_types, SORT_ASC, $zones);
0 ignored issues
show
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

6328
	array_multisort($offsets, /** @scrutinizer ignore-type */ SORT_ASC, SORT_NUMERIC, $labels, SORT_ASC, $dst_types, 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

6328
	array_multisort($offsets, SORT_ASC, /** @scrutinizer ignore-type */ SORT_NUMERIC, $labels, SORT_ASC, $dst_types, SORT_ASC, $zones);
Loading history...
Comprehensibility Best Practice introduced by
The variable $zones does not seem to be defined for all execution paths leading up to this point.
Loading history...
6329
6330
	// Build the final array of formatted values
6331
	$priority_timezones = array();
6332
	$timezones = array();
6333
	foreach ($zones as $tzkey => $tzvalue)
6334
	{
6335
		date_timezone_set($date_when, timezone_open($tzvalue['tzid']));
6336
6337
		// Use the human friendly time zone name, if there is one.
6338
		$desc = '';
6339
		if (!empty($tzvalue['metazone']))
6340
		{
6341
			if (!empty($tztxt[$tzvalue['metazone']]))
6342
				$metazone = $tztxt[$tzvalue['metazone']];
6343
			else
6344
				$metazone = sprintf($tztxt['generic_timezone'], $tzvalue['metazone'], '%1$s');
6345
6346
			switch ($tzvalue['dst_type'])
6347
			{
6348
				case 0:
6349
					$desc = sprintf($metazone, $tztxt['daylight_saving_time_false']);
6350
					break;
6351
6352
				case 1:
6353
					$desc = sprintf($metazone, '');
6354
					break;
6355
6356
				case 2:
6357
					$desc = sprintf($metazone, $tztxt['daylight_saving_time_true']);
6358
					break;
6359
			}
6360
		}
6361
		// Otherwise, use the list of locations (max 5, so things don't get silly)
6362
		else
6363
			$desc = implode(', ', array_slice(array_unique($tzvalue['locations']), 0, 5)) . (count($tzvalue['locations']) > 5 ? ', ' . $txt['etc'] : '');
6364
6365
		// We don't want abbreviations like '+03' or '-11'.
6366
		$abbrs = array_filter($tzvalue['abbrs'], function ($abbr) {
6367
			return !strspn($abbr, '+-');
6368
		});
6369
		$abbrs = count($abbrs) == count($tzvalue['abbrs']) ? array_unique($abbrs) : array();
6370
6371
		// Show the UTC offset and abbreviation(s).
6372
		$desc = '[UTC' . date_format($date_when, 'P') . '] - ' . str_replace('  ', ' ', $desc) . (!empty($abbrs) ? ' (' . implode('/', $abbrs) . ')' : '');
6373
6374
		if (isset($priority_zones[$tzkey]))
6375
			$priority_timezones[$tzvalue['tzid']] = $desc;
6376
		else
6377
			$timezones[$tzvalue['tzid']] = $desc;
6378
6379
		// Automatically fix orphaned time zones.
6380
		if (isset($member_tzkey) && $member_tzkey == $tzkey)
6381
			$cur_profile['timezone'] = $tzvalue['tzid'];
6382
		if (isset($event_tzkey) && $event_tzkey == $tzkey)
6383
			$context['event']['tz'] = $tzvalue['tzid'];
6384
	}
6385
6386
	if (!empty($priority_timezones))
6387
		$priority_timezones[] = '-----';
6388
6389
	$timezones = array_merge(
6390
		$priority_timezones,
6391
		array('UTC' => 'UTC' . (!empty($tztxt['UTC']) ? ' - ' . $tztxt['UTC'] : ''), '-----'),
6392
		$timezones
6393
	);
6394
6395
	$timezones_when[$when] = $timezones;
6396
6397
	return $timezones_when[$when];
6398
}
6399
6400
/**
6401
 * Gets a member's selected time zone identifier
6402
 *
6403
 * @param int $id_member The member id to look up. If not provided, the current user's id will be used.
6404
 * @return string The time zone identifier string for the user's time zone.
6405
 */
6406
function getUserTimezone($id_member = null)
6407
{
6408
	global $smcFunc, $context, $user_info, $modSettings, $user_settings;
6409
	static $member_cache = array();
6410
6411
	if (is_null($id_member) && $user_info['is_guest'] == false)
6412
		$id_member = $context['user']['id'];
6413
6414
	// Did we already look this up?
6415
	if (isset($id_member) && isset($member_cache[$id_member]))
6416
	{
6417
		return $member_cache[$id_member];
6418
	}
6419
6420
	// Check if we already have this in $user_settings.
6421
	if (isset($user_settings['id_member']) && $user_settings['id_member'] == $id_member && !empty($user_settings['timezone']))
6422
	{
6423
		$member_cache[$id_member] = $user_settings['timezone'];
6424
		return $user_settings['timezone'];
6425
	}
6426
6427
	// Look it up in the database.
6428
	if (isset($id_member))
6429
	{
6430
		$request = $smcFunc['db_query']('', '
6431
			SELECT timezone
6432
			FROM {db_prefix}members
6433
			WHERE id_member = {int:id_member}',
6434
			array(
6435
				'id_member' => $id_member,
6436
			)
6437
		);
6438
		list($timezone) = $smcFunc['db_fetch_row']($request);
6439
		$smcFunc['db_free_result']($request);
6440
	}
6441
6442
	// If it is invalid, fall back to the default.
6443
	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

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

6933
		if (md5($tlds) != substr(/** @scrutinizer ignore-type */ $tlds_md5, 0, 32))
Loading history...
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

6933
		if (md5(/** @scrutinizer ignore-type */ $tlds) != substr($tlds_md5, 0, 32))
Loading history...
6934
			$tlds = array();
6935
	}
6936
	// If we aren't updating and the regex is valid, we're done
6937
	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

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

7246
	$url = /** @scrutinizer ignore-type */ str_ireplace('https://', 'http://', $url) . '/';
Loading history...
7247
	$headers = @get_headers($url);
7248
	if ($headers === false)
7249
		return false;
7250
7251
	// Now to see if it came back https...
7252
	// First check for a redirect status code in first row (301, 302, 307)
7253
	if (strstr($headers[0], '301') === false && strstr($headers[0], '302') === false && strstr($headers[0], '307') === false)
7254
		return false;
7255
7256
	// Search for the location entry to confirm https
7257
	$result = false;
7258
	foreach ($headers as $header)
7259
	{
7260
		if (stristr($header, 'Location: https://') !== false)
7261
		{
7262
			$result = true;
7263
			break;
7264
		}
7265
	}
7266
	return $result;
7267
}
7268
7269
/**
7270
 * Build query_wanna_see_board and query_see_board for a userid
7271
 *
7272
 * Returns array with keys query_wanna_see_board and query_see_board
7273
 *
7274
 * @param int $userid of the user
7275
 */
7276
function build_query_board($userid)
7277
{
7278
	global $user_info, $modSettings, $smcFunc, $db_prefix;
7279
7280
	$query_part = array();
7281
7282
	// If we come from cron, we can't have a $user_info.
7283
	if (isset($user_info['id']) && $user_info['id'] == $userid && SMF != 'BACKGROUND')
7284
	{
7285
		$groups = $user_info['groups'];
7286
		$can_see_all_boards = $user_info['is_admin'] || $user_info['can_manage_boards'];
7287
		$ignoreboards = !empty($user_info['ignoreboards']) ? $user_info['ignoreboards'] : null;
7288
	}
7289
	else
7290
	{
7291
		$request = $smcFunc['db_query']('', '
7292
			SELECT mem.ignore_boards, mem.id_group, mem.additional_groups, mem.id_post_group
7293
			FROM {db_prefix}members AS mem
7294
			WHERE mem.id_member = {int:id_member}
7295
			LIMIT 1',
7296
			array(
7297
				'id_member' => $userid,
7298
			)
7299
		);
7300
7301
		$row = $smcFunc['db_fetch_assoc']($request);
7302
7303
		if (empty($row['additional_groups']))
7304
			$groups = array($row['id_group'], $row['id_post_group']);
7305
		else
7306
			$groups = array_merge(
7307
				array($row['id_group'], $row['id_post_group']),
7308
				explode(',', $row['additional_groups'])
7309
			);
7310
7311
		// Because history has proven that it is possible for groups to go bad - clean up in case.
7312
		foreach ($groups as $k => $v)
7313
			$groups[$k] = (int) $v;
7314
7315
		$can_see_all_boards = in_array(1, $groups) || (!empty($modSettings['board_manager_groups']) && count(array_intersect($groups, explode(',', $modSettings['board_manager_groups']))) > 0);
7316
7317
		$ignoreboards = !empty($row['ignore_boards']) && !empty($modSettings['allow_ignore_boards']) ? explode(',', $row['ignore_boards']) : array();
7318
	}
7319
7320
	// Just build this here, it makes it easier to change/use - administrators can see all boards.
7321
	if ($can_see_all_boards)
7322
		$query_part['query_see_board'] = '1=1';
7323
	// Otherwise just the groups in $user_info['groups'].
7324
	else
7325
	{
7326
		$query_part['query_see_board'] = '
7327
			EXISTS (
7328
				SELECT bpv.id_board
7329
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
7330
				WHERE bpv.id_group IN ('. implode(',', $groups) .')
7331
					AND bpv.deny = 0
7332
					AND bpv.id_board = b.id_board
7333
			)';
7334
7335
		if (!empty($modSettings['deny_boards_access']))
7336
			$query_part['query_see_board'] .= '
7337
			AND NOT EXISTS (
7338
				SELECT bpv.id_board
7339
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
7340
				WHERE bpv.id_group IN ( '. implode(',', $groups) .')
7341
					AND bpv.deny = 1
7342
					AND bpv.id_board = b.id_board
7343
			)';
7344
	}
7345
7346
	$query_part['query_see_message_board'] = str_replace('b.', 'm.', $query_part['query_see_board']);
7347
	$query_part['query_see_topic_board'] = str_replace('b.', 't.', $query_part['query_see_board']);
7348
7349
	// Build the list of boards they WANT to see.
7350
	// This will take the place of query_see_boards in certain spots, so it better include the boards they can see also
7351
7352
	// If they aren't ignoring any boards then they want to see all the boards they can see
7353
	if (empty($ignoreboards))
7354
	{
7355
		$query_part['query_wanna_see_board'] = $query_part['query_see_board'];
7356
		$query_part['query_wanna_see_message_board'] = $query_part['query_see_message_board'];
7357
		$query_part['query_wanna_see_topic_board'] = $query_part['query_see_topic_board'];
7358
	}
7359
	// Ok I guess they don't want to see all the boards
7360
	else
7361
	{
7362
		$query_part['query_wanna_see_board'] = '(' . $query_part['query_see_board'] . ' AND b.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7363
		$query_part['query_wanna_see_message_board'] = '(' . $query_part['query_see_message_board'] . ' AND m.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7364
		$query_part['query_wanna_see_topic_board'] = '(' . $query_part['query_see_topic_board'] . ' AND t.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7365
	}
7366
7367
	return $query_part;
7368
}
7369
7370
/**
7371
 * Check if the connection is using https.
7372
 *
7373
 * @return boolean true if connection used https
7374
 */
7375
function httpsOn()
7376
{
7377
	$secure = false;
7378
7379
	if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on')
7380
		$secure = true;
7381
	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...
7382
		$secure = true;
7383
7384
	return $secure;
7385
}
7386
7387
/**
7388
 * A wrapper for `filter_var($url, FILTER_VALIDATE_URL)` that can handle URLs
7389
 * with international characters (a.k.a. IRIs)
7390
 *
7391
 * @param string $iri The IRI to test.
7392
 * @param int $flags Optional flags to pass to filter_var()
7393
 * @return string|bool Either the original IRI, or false if the IRI was invalid.
7394
 */
7395
function validate_iri($iri, $flags = null)
7396
{
7397
	$url = iri_to_url($iri);
7398
7399
	// PHP 5 doesn't recognize IPv6 addresses in the URL host.
7400
	if (version_compare(phpversion(), '7.0.0', '<'))
7401
	{
7402
		$host = parse_url((strpos($url, '//') === 0 ? 'http:' : '') . $url, PHP_URL_HOST);
7403
7404
		if (strpos($host, '[') === 0 && strpos($host, ']') === strlen($host) && strpos($host, ':') !== false)
7405
			$url = str_replace($host, '127.0.0.1', $url);
7406
	}
7407
7408
	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

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

7696
				$value = substr($value, 0, /** @scrutinizer ignore-type */ $param_max - strlen($key) - 5);
Loading history...
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

7696
				$value = substr(/** @scrutinizer ignore-type */ $value, 0, $param_max - strlen($key) - 5);
Loading history...
7697
		}
7698
7699
		return $array;
7700
	}
7701
}
7702
7703
/**
7704
 * array_length Recursive
7705
 * @param array $array
7706
 * @param int $deep How many levels should the function
7707
 * @return int
7708
 */
7709
function array_length($array, $deep = 3)
7710
{
7711
	// Work with arrays
7712
	$array = (array) $array;
7713
	$length = 0;
7714
7715
	$deep_count = $deep - 1;
7716
7717
	foreach ($array as $value)
7718
	{
7719
		// Recursive?
7720
		if (is_array($value))
7721
		{
7722
			// No can't do
7723
			if ($deep_count <= 0)
7724
				continue;
7725
7726
			$length += array_length($value, $deep_count);
7727
		}
7728
		else
7729
			$length += strlen($value);
7730
	}
7731
7732
	return $length;
7733
}
7734
7735
/**
7736
 * Compares existance request variables against an array.
7737
 *
7738
 * The input array is associative, where keys denote accepted values
7739
 * in a request variable denoted by `$req_val`. Values can be:
7740
 *
7741
 * - another associative array where at least one key must be found
7742
 *   in the request and their values are accepted request values.
7743
 * - A scalar value, in which case no furthur checks are done.
7744
 *
7745
 * @param array $array
7746
 * @param string $req_var request variable
7747
 *
7748
 * @return bool whether any of the criteria was satisfied
7749
 */
7750
function is_filtered_request(array $array, $req_var)
7751
{
7752
	$matched = false;
7753
	if (isset($_REQUEST[$req_var], $array[$_REQUEST[$req_var]]))
7754
	{
7755
		if (is_array($array[$_REQUEST[$req_var]]))
7756
		{
7757
			foreach ($array[$_REQUEST[$req_var]] as $subtype => $subnames)
7758
				$matched |= isset($_REQUEST[$subtype]) && in_array($_REQUEST[$subtype], $subnames);
7759
		}
7760
		else
7761
			$matched = true;
7762
	}
7763
7764
	return (bool) $matched;
7765
}
7766
7767
/**
7768
 * Clean up the XML to make sure it doesn't contain invalid characters.
7769
 *
7770
 * See https://www.w3.org/TR/xml/#charsets
7771
 *
7772
 * @param string $string The string to clean
7773
 * @return string The cleaned string
7774
 */
7775
function cleanXml($string)
7776
{
7777
	global $context;
7778
7779
	$illegal_chars = array(
7780
		// Remove all ASCII control characters except \t, \n, and \r.
7781
		"\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08",
7782
		"\x0B", "\x0C", "\x0E", "\x0F", "\x10", "\x11", "\x12", "\x13", "\x14",
7783
		"\x15", "\x16", "\x17", "\x18", "\x19", "\x1A", "\x1B", "\x1C", "\x1D",
7784
		"\x1E", "\x1F",
7785
		// Remove \xFFFE and \xFFFF
7786
		"\xEF\xBF\xBE", "\xEF\xBF\xBF",
7787
	);
7788
7789
	$string = str_replace($illegal_chars, '', $string);
7790
7791
	// The Unicode surrogate pair code points should never be present in our
7792
	// strings to begin with, but if any snuck in, they need to be removed.
7793
	if (!empty($context['utf8']) && strpos($string, "\xED") !== false)
7794
		$string = preg_replace('/\xED[\xA0-\xBF][\x80-\xBF]/', '', $string);
7795
7796
	return $string;
7797
}
7798
7799
/**
7800
 * Escapes (replaces) characters in strings to make them safe for use in javascript
7801
 *
7802
 * @param string $string The string to escape
7803
 * @return string The escaped string
7804
 */
7805
function JavaScriptEscape($string)
7806
{
7807
	global $scripturl;
7808
7809
	return '\'' . strtr($string, array(
7810
		"\r" => '',
7811
		"\n" => '\\n',
7812
		"\t" => '\\t',
7813
		'\\' => '\\\\',
7814
		'\'' => '\\\'',
7815
		'</' => '<\' + \'/',
7816
		'<script' => '<scri\'+\'pt',
7817
		'<body>' => '<bo\'+\'dy>',
7818
		'<a href' => '<a hr\'+\'ef',
7819
		$scripturl => '\' + smf_scripturl + \'',
7820
	)) . '\'';
7821
}
7822
7823
?>