Passed
Pull Request — release-2.1 (#6754)
by Jon
05:25
created

_safe_unserialize()   F

Complexity

Conditions 34
Paths 341

Size

Total Lines 142
Code Lines 76

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 34
eloc 76
nc 341
nop 1
dl 0
loc 142
rs 1.5708
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file has all the main functions in it that relate to, well, everything.
5
 *
6
 * Simple Machines Forum (SMF)
7
 *
8
 * @package SMF
9
 * @author Simple Machines 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
							// Schemes that we should never link.
2414
							'forbidden' => array(
2415
								'javascript', 'invalid',
2416
							),
2417
						);
2418
2419
						// In case a mod wants to control behaviour for a special URI scheme.
2420
						call_integration_hook('integrate_autolinker_schemes', array(&$schemes));
2421
2422
						// Don't repeat this unnecessarily.
2423
						if (empty($url_regex))
2424
						{
2425
							// PCRE subroutines for efficiency.
2426
							$pcre_subroutines = array(
2427
								'tlds' => $modSettings['tld_regex'],
2428
								'pct' => '%[0-9A-Fa-f]{2}',
2429
								'domain_label_char' => '[' . $domain_label_chars . ']',
2430
								'not_domain_label_char' => '[^' . $domain_label_chars . ']',
2431
								'domain' => '(?:(?P>domain_label_char)+\.)+(?P>tlds)',
2432
								'no_domain' => '(?:(?P>domain_label_char)|[._\~!$&\'()*+,;=:@]|(?P>pct))+',
2433
								'scheme_need_domain' => build_regex($schemes['need_domain'], '~'),
2434
								'scheme_empty_authority' => build_regex($schemes['empty_authority'], '~'),
2435
								'scheme_no_authority' => build_regex($schemes['no_authority'], '~'),
2436
								'scheme_any' => '[A-Za-z][0-9A-Za-z+\-.]*',
2437
								'user_info' => '(?:(?P>domain_label_char)|[._\~!$&\'()*+,;=:]|(?P>pct))+',
2438
								'dec_octet' => '(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)',
2439
								'h16' => '[0-9A-Fa-f]{1,4}',
2440
								'ipv4' => '(?:\b(?:(?P>dec_octet)\.){3}(?P>dec_octet)\b)',
2441
								'ipv6' => '\[(?:' . implode('|', array(
2442
									'(?:(?P>h16):){7}(?P>h16)',
2443
									'(?:(?P>h16):){1,7}:',
2444
									'(?:(?P>h16):){1,6}(?::(?P>h16))',
2445
									'(?:(?P>h16):){1,5}(?::(?P>h16)){1,2}',
2446
									'(?:(?P>h16):){1,4}(?::(?P>h16)){1,3}',
2447
									'(?:(?P>h16):){1,3}(?::(?P>h16)){1,4}',
2448
									'(?:(?P>h16):){1,2}(?::(?P>h16)){1,5}',
2449
									'(?P>h16):(?::(?P>h16)){1,6}',
2450
									':(?:(?::(?P>h16)){1,7}|:)',
2451
									'fe80:(?::(?P>h16)){0,4}%[0-9A-Za-z]+',
2452
									'::(ffff(:0{1,4})?:)?(?P>ipv4)',
2453
									'(?:(?P>h16):){1,4}:(?P>ipv4)',
2454
								)) . ')\]',
2455
								'host' => '(?:' . implode('|', array(
2456
									'localhost',
2457
									'(?P>domain)',
2458
									'(?P>ipv4)',
2459
									'(?P>ipv6)',
2460
								)) . ')',
2461
								'authority' => '(?:(?P>user_info)@)?(?P>host)(?::\d+)?',
2462
							);
2463
2464
							// Brackets and quotation marks are problematic at the end of an IRI.
2465
							// E.g.: `http://foo.com/baz(qux)` vs. `(http://foo.com/baz_qux)`
2466
							// In the first case, the user probably intended the `)` as part of the
2467
							// IRI, but not in the second case. To account for this, we test for
2468
							// balanced pairs within the IRI.
2469
							$balanced_pairs = array(
2470
								// Brackets and parentheses
2471
								'(' => ')', '[' => ']', '{' => '}',
2472
								// Double quotation marks
2473
								'"' => '"',
2474
								html_entity_decode('&#x201C;') => html_entity_decode('&#x201D;'),
2475
								html_entity_decode('&#x201E;') => html_entity_decode('&#x201D;'),
2476
								html_entity_decode('&#x201F;') => html_entity_decode('&#x201D;'),
2477
								html_entity_decode('&#x00AB;') => html_entity_decode('&#x00BB;'),
2478
								// Single quotation marks
2479
								'\'' => '\'',
2480
								html_entity_decode('&#x2018;') => html_entity_decode('&#x2019;'),
2481
								html_entity_decode('&#x201A;') => html_entity_decode('&#x2019;'),
2482
								html_entity_decode('&#x201B;') => html_entity_decode('&#x2019;'),
2483
								html_entity_decode('&#x2039;') => html_entity_decode('&#x203A;'),
2484
							);
2485
							foreach ($balanced_pairs as $pair_opener => $pair_closer)
2486
								$balanced_pairs[$smcFunc['htmlspecialchars']($pair_opener)] = $smcFunc['htmlspecialchars']($pair_closer);
2487
2488
							$bracket_quote_chars = '';
2489
							$bracket_quote_entities = array();
2490
							foreach ($balanced_pairs as $pair_opener => $pair_closer)
2491
							{
2492
								if ($pair_opener == $pair_closer)
2493
									$pair_closer = '';
2494
2495
								foreach (array($pair_opener, $pair_closer) as $bracket_quote)
2496
								{
2497
									if (strpos($bracket_quote, '&') === false)
2498
										$bracket_quote_chars .= $bracket_quote;
2499
									else
2500
										$bracket_quote_entities[] = substr($bracket_quote, 1);
2501
								}
2502
							}
2503
							$bracket_quote_chars = str_replace(array('[', ']'), array('\[', '\]'), $bracket_quote_chars);
2504
2505
							$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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

6070
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
6071
		// all others get html-ised
6072
		else
6073
			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

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

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

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

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

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

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

}

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

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

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

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

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

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

6332
	array_multisort($offsets, /** @scrutinizer ignore-type */ SORT_ASC, 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...
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

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

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

6937
		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

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

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

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

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

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

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