Passed
Push — release-2.1 ( 8f939c...c09c8c )
by Mathias
06:25
created

text2words()   C

Complexity

Conditions 12

Size

Total Lines 44
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 23
nop 3
dl 0
loc 44
rs 6.9666
c 1
b 0
f 0

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

710
	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...
711
}
712
713
/**
714
 * Format a time to make it look purdy.
715
 *
716
 * - returns a pretty formatted version of time based on the user's format in $user_info['time_format'].
717
 * - applies all necessary time offsets to the timestamp, unless offset_type is set.
718
 * - if todayMod is set and show_today was not not specified or true, an
719
 *   alternate format string is used to show the date with something to show it is "today" or "yesterday".
720
 * - performs localization (more than just strftime would do alone.)
721
 *
722
 * @param int $log_time A timestamp
723
 * @param bool $show_today Whether to show "Today"/"Yesterday" or just a date
724
 * @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.
725
 * @param bool $process_safe Activate setlocale check for changes at runtime. Slower, but safer.
726
 * @return string A formatted timestamp
727
 */
728
function timeformat($log_time, $show_today = true, $offset_type = false, $process_safe = false)
729
{
730
	global $context, $user_info, $txt, $modSettings;
731
	static $non_twelve_hour, $locale, $now;
732
	static $unsupportedFormats, $finalizedFormats;
733
734
	$unsupportedFormatsWindows = array('z', 'Z');
735
736
	// Ensure required values are set
737
	$user_info['time_offset'] = !empty($user_info['time_offset']) ? $user_info['time_offset'] : 0;
738
	$modSettings['time_offset'] = !empty($modSettings['time_offset']) ? $modSettings['time_offset'] : 0;
739
	$user_info['time_format'] = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %H:%M');
740
741
	// Offset the time.
742
	if (!$offset_type)
743
		$log_time = $log_time + ($user_info['time_offset'] + $modSettings['time_offset']) * 3600;
744
	// Just the forum offset?
745
	elseif ($offset_type == 'forum')
746
		$log_time = $log_time + $modSettings['time_offset'] * 3600;
747
748
	// We can't have a negative date (on Windows, at least.)
749
	if ($log_time < 0)
750
		$log_time = 0;
751
752
	// Today and Yesterday?
753
	$prefix = '';
754
	if ($modSettings['todayMod'] >= 1 && $show_today === true)
755
	{
756
		$now_time = forum_time();
757
758
		if ($now_time - $log_time < (86400 * $modSettings['todayMod']))
759
		{
760
			$then = @getdate($log_time);
761
			$now = (!empty($now) ? $now : @getdate($now_time));
762
763
			// Same day of the year, same year.... Today!
764
			if ($then['yday'] == $now['yday'] && $then['year'] == $now['year'])
765
			{
766
				$prefix = $txt['today'];
767
			}
768
			// 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...
769
			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))
770
			{
771
				$prefix = $txt['yesterday'];
772
			}
773
		}
774
	}
775
776
	$str = !is_bool($show_today) ? $show_today : $user_info['time_format'];
0 ignored issues
show
introduced by
The condition is_bool($show_today) is always true.
Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $format_parts can also be of type false; however, parameter $pieces of implode() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

1035
	$format = implode('', /** @scrutinizer ignore-type */ $format_parts);
Loading history...
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
	if ($timestamp === null)
1136
		$timestamp = time();
1137
	elseif ($timestamp == 0)
1138
		return 0;
1139
1140
	return $timestamp + ($modSettings['time_offset'] + ($use_user_offset ? $user_info['time_offset'] : 0)) * 3600;
1141
}
1142
1143
/**
1144
 * Calculates all the possible permutations (orders) of array.
1145
 * should not be called on huge arrays (bigger than like 10 elements.)
1146
 * returns an array containing each permutation.
1147
 *
1148
 * @deprecated since 2.1
1149
 * @param array $array An array
1150
 * @return array An array containing each permutation
1151
 */
1152
function permute($array)
1153
{
1154
	$orders = array($array);
1155
1156
	$n = count($array);
1157
	$p = range(0, $n);
1158
	for ($i = 1; $i < $n; null)
1159
	{
1160
		$p[$i]--;
1161
		$j = $i % 2 != 0 ? $p[$i] : 0;
1162
1163
		$temp = $array[$i];
1164
		$array[$i] = $array[$j];
1165
		$array[$j] = $temp;
1166
1167
		for ($i = 1; $p[$i] == 0; $i++)
1168
			$p[$i] = 1;
1169
1170
		$orders[] = $array;
1171
	}
1172
1173
	return $orders;
1174
}
1175
1176
/**
1177
 * Parse bulletin board code in a string, as well as smileys optionally.
1178
 *
1179
 * - only parses bbc tags which are not disabled in disabledBBC.
1180
 * - handles basic HTML, if enablePostHTML is on.
1181
 * - caches the from/to replace regular expressions so as not to reload them every time a string is parsed.
1182
 * - only parses smileys if smileys is true.
1183
 * - does nothing if the enableBBC setting is off.
1184
 * - uses the cache_id as a unique identifier to facilitate any caching it may do.
1185
 * - returns the modified message.
1186
 *
1187
 * @param string|bool $message The message.
1188
 *		When a empty string, nothing is done.
1189
 *		When false we provide a list of BBC codes available.
1190
 *		When a string, the message is parsed and bbc handled.
1191
 * @param bool $smileys Whether to parse smileys as well
1192
 * @param string $cache_id The cache ID
1193
 * @param array $parse_tags If set, only parses these tags rather than all of them
1194
 * @return string The parsed message
1195
 */
1196
function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = array())
1197
{
1198
	global $smcFunc, $txt, $scripturl, $context, $modSettings, $user_info, $sourcedir, $cache_enable;
1199
	static $bbc_lang_locales = array(), $itemcodes = array(), $no_autolink_tags = array();
1200
	static $disabled, $alltags_regex = '', $param_regexes = array();
1201
1202
	// Don't waste cycles
1203
	if ($message === '')
1204
		return '';
1205
1206
	// Just in case it wasn't determined yet whether UTF-8 is enabled.
1207
	if (!isset($context['utf8']))
1208
		$context['utf8'] = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8';
1209
1210
	// Clean up any cut/paste issues we may have
1211
	$message = sanitizeMSCutPaste($message);
1212
1213
	// If the load average is too high, don't parse the BBC.
1214
	if (!empty($context['load_average']) && !empty($modSettings['bbc']) && $context['load_average'] >= $modSettings['bbc'])
1215
	{
1216
		$context['disabled_parse_bbc'] = true;
1217
		return $message;
1218
	}
1219
1220
	if ($smileys !== null && ($smileys == '1' || $smileys == '0'))
1221
		$smileys = (bool) $smileys;
1222
1223
	if (empty($modSettings['enableBBC']) && $message !== false)
1224
	{
1225
		if ($smileys === true)
1226
			parsesmileys($message);
1227
1228
		return $message;
1229
	}
1230
1231
	// If we already have a version of the BBCodes for the current language, use that. Otherwise, make one.
1232
	if (!empty($bbc_lang_locales[$txt['lang_locale']]))
1233
		$bbc_codes = $bbc_lang_locales[$txt['lang_locale']];
1234
	else
1235
		$bbc_codes = array();
1236
1237
	// If we are not doing every tag then we don't cache this run.
1238
	if (!empty($parse_tags))
1239
		$bbc_codes = array();
1240
1241
	// Ensure $modSettings['tld_regex'] contains a valid regex for the autolinker
1242
	if (!empty($modSettings['autoLinkUrls']))
1243
		set_tld_regex();
1244
1245
	// Allow mods access before entering the main parse_bbc loop
1246
	call_integration_hook('integrate_pre_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
1247
1248
	// Sift out the bbc for a performance improvement.
1249
	if (empty($bbc_codes) || $message === false || !empty($parse_tags))
1250
	{
1251
		if (!empty($modSettings['disabledBBC']))
1252
		{
1253
			$disabled = array();
1254
1255
			$temp = explode(',', strtolower($modSettings['disabledBBC']));
1256
1257
			foreach ($temp as $tag)
1258
				$disabled[trim($tag)] = true;
1259
1260
			if (in_array('color', $disabled))
1261
				$disabled = array_merge($disabled, array(
1262
					'black' => true,
1263
					'white' => true,
1264
					'red' => true,
1265
					'green' => true,
1266
					'blue' => true,
1267
					)
1268
				);
1269
		}
1270
1271
		// The YouTube bbc needs this for its origin parameter
1272
		$scripturl_parts = parse_url($scripturl);
1273
		$hosturl = $scripturl_parts['scheme'] . '://' . $scripturl_parts['host'];
1274
1275
		/* The following bbc are formatted as an array, with keys as follows:
1276
1277
			tag: the tag's name - should be lowercase!
1278
1279
			type: one of...
1280
				- (missing): [tag]parsed content[/tag]
1281
				- unparsed_equals: [tag=xyz]parsed content[/tag]
1282
				- parsed_equals: [tag=parsed data]parsed content[/tag]
1283
				- unparsed_content: [tag]unparsed content[/tag]
1284
				- closed: [tag], [tag/], [tag /]
1285
				- unparsed_commas: [tag=1,2,3]parsed content[/tag]
1286
				- unparsed_commas_content: [tag=1,2,3]unparsed content[/tag]
1287
				- unparsed_equals_content: [tag=...]unparsed content[/tag]
1288
1289
			parameters: an optional array of parameters, for the form
1290
			  [tag abc=123]content[/tag].  The array is an associative array
1291
			  where the keys are the parameter names, and the values are an
1292
			  array which may contain the following:
1293
				- match: a regular expression to validate and match the value.
1294
				- quoted: true if the value should be quoted.
1295
				- validate: callback to evaluate on the data, which is $data.
1296
				- value: a string in which to replace $1 with the data.
1297
					Either value or validate may be used, not both.
1298
				- optional: true if the parameter is optional.
1299
				- default: a default value for missing optional parameters.
1300
1301
			test: a regular expression to test immediately after the tag's
1302
			  '=', ' ' or ']'.  Typically, should have a \] at the end.
1303
			  Optional.
1304
1305
			content: only available for unparsed_content, closed,
1306
			  unparsed_commas_content, and unparsed_equals_content.
1307
			  $1 is replaced with the content of the tag.  Parameters
1308
			  are replaced in the form {param}.  For unparsed_commas_content,
1309
			  $2, $3, ..., $n are replaced.
1310
1311
			before: only when content is not used, to go before any
1312
			  content.  For unparsed_equals, $1 is replaced with the value.
1313
			  For unparsed_commas, $1, $2, ..., $n are replaced.
1314
1315
			after: similar to before in every way, except that it is used
1316
			  when the tag is closed.
1317
1318
			disabled_content: used in place of content when the tag is
1319
			  disabled.  For closed, default is '', otherwise it is '$1' if
1320
			  block_level is false, '<div>$1</div>' elsewise.
1321
1322
			disabled_before: used in place of before when disabled.  Defaults
1323
			  to '<div>' if block_level, '' if not.
1324
1325
			disabled_after: used in place of after when disabled.  Defaults
1326
			  to '</div>' if block_level, '' if not.
1327
1328
			block_level: set to true the tag is a "block level" tag, similar
1329
			  to HTML.  Block level tags cannot be nested inside tags that are
1330
			  not block level, and will not be implicitly closed as easily.
1331
			  One break following a block level tag may also be removed.
1332
1333
			trim: if set, and 'inside' whitespace after the begin tag will be
1334
			  removed.  If set to 'outside', whitespace after the end tag will
1335
			  meet the same fate.
1336
1337
			validate: except when type is missing or 'closed', a callback to
1338
			  validate the data as $data.  Depending on the tag's type, $data
1339
			  may be a string or an array of strings (corresponding to the
1340
			  replacement.)
1341
1342
			quoted: when type is 'unparsed_equals' or 'parsed_equals' only,
1343
			  may be not set, 'optional', or 'required' corresponding to if
1344
			  the content may be quoted.  This allows the parser to read
1345
			  [tag="abc]def[esdf]"] properly.
1346
1347
			require_parents: an array of tag names, or not set.  If set, the
1348
			  enclosing tag *must* be one of the listed tags, or parsing won't
1349
			  occur.
1350
1351
			require_children: similar to require_parents, if set children
1352
			  won't be parsed if they are not in the list.
1353
1354
			disallow_children: similar to, but very different from,
1355
			  require_children, if it is set the listed tags will not be
1356
			  parsed inside the tag.
1357
1358
			parsed_tags_allowed: an array restricting what BBC can be in the
1359
			  parsed_equals parameter, if desired.
1360
		*/
1361
1362
		$codes = array(
1363
			array(
1364
				'tag' => 'abbr',
1365
				'type' => 'unparsed_equals',
1366
				'before' => '<abbr title="$1">',
1367
				'after' => '</abbr>',
1368
				'quoted' => 'optional',
1369
				'disabled_after' => ' ($1)',
1370
			),
1371
			// Legacy (and just an alias for [abbr] even when enabled)
1372
			array(
1373
				'tag' => 'acronym',
1374
				'type' => 'unparsed_equals',
1375
				'before' => '<abbr title="$1">',
1376
				'after' => '</abbr>',
1377
				'quoted' => 'optional',
1378
				'disabled_after' => ' ($1)',
1379
			),
1380
			array(
1381
				'tag' => 'anchor',
1382
				'type' => 'unparsed_equals',
1383
				'test' => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]',
1384
				'before' => '<span id="post_$1">',
1385
				'after' => '</span>',
1386
			),
1387
			array(
1388
				'tag' => 'attach',
1389
				'type' => 'unparsed_content',
1390
				'parameters' => array(
1391
					'id' => array('match' => '(\d+)'),
1392
					'alt' => array('optional' => true),
1393
					'width' => array('optional' => true, 'match' => '(\d+)'),
1394
					'height' => array('optional' => true, 'match' => '(\d+)'),
1395
					'display' => array('optional' => true, 'match' => '(link|embed)'),
1396
				),
1397
				'content' => '$1',
1398
				'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...
1399
				{
1400
					$returnContext = '';
1401
1402
					// BBC or the entire attachments feature is disabled
1403
					if (empty($modSettings['attachmentEnable']) || !empty($disabled['attach']))
1404
						return $data;
1405
1406
					// Save the attach ID.
1407
					$attachID = $params['{id}'];
1408
1409
					// Kinda need this.
1410
					require_once($sourcedir . '/Subs-Attachments.php');
1411
1412
					$currentAttachment = parseAttachBBC($attachID);
1413
1414
					// parseAttachBBC will return a string ($txt key) rather than dying with a fatal_error. Up to you to decide what to do.
1415
					if (is_string($currentAttachment))
1416
						return $data = !empty($txt[$currentAttachment]) ? $txt[$currentAttachment] : $currentAttachment;
1417
1418
					// We need a display mode.
1419
					if (empty($params['{display}']))
1420
					{
1421
						// Images, video, and audio are embedded by default.
1422
						if (!empty($currentAttachment['is_image']) || strpos($currentAttachment['mime_type'], 'video/') === 0 || strpos($currentAttachment['mime_type'], 'audio/') === 0)
1423
							$params['{display}'] = 'embed';
1424
						// Anything else shows a link by default.
1425
						else
1426
							$params['{display}'] = 'link';
1427
					}
1428
1429
					// Embedded file.
1430
					if ($params['{display}'] == 'embed')
1431
					{
1432
						$alt = ' alt="' . (!empty($params['{alt}']) ? $params['{alt}'] : $currentAttachment['name']) . '"';
1433
						$title = !empty($data) ? ' title="' . $smcFunc['htmlspecialchars']($data) . '"' : '';
1434
1435
						$width = !empty($params['{width}']) ? $params['{width}'] : (!empty($currentAttachment['width']) ? $currentAttachment['width'] : '');
1436
						$height = !empty($params['{height}']) ? $params['{height}'] : (!empty($currentAttachment['height']) ? $currentAttachment['height'] : '');
1437
1438
						// Image.
1439
						if (!empty($currentAttachment['is_image']))
1440
						{
1441
							$width = !empty($width) ? ' width="' . $width . '"' : '';
1442
							$height = !empty($height) ? ' height="' . $height . '"' : '';
1443
1444
							if ($currentAttachment['thumbnail']['has_thumb'] && empty($params['{width}']) && empty($params['{height}']))
1445
								$returnContext .= '<a href="' . $currentAttachment['href'] . ';image" id="link_' . $currentAttachment['id'] . '" onclick="' . $currentAttachment['thumbnail']['javascript'] . '"><img src="' . $currentAttachment['thumbnail']['href'] . '"' . $alt . $title . ' id="thumb_' . $currentAttachment['id'] . '" class="atc_img"></a>';
1446
							else
1447
								$returnContext .= '<img src="' . $currentAttachment['href'] . ';image"' . $alt . $title . $width . $height . ' class="bbc_img"/>';
1448
						}
1449
						// Video.
1450
						elseif (strpos($currentAttachment['mime_type'], 'video/') === 0)
1451
						{
1452
							$width = !empty($width) ? ' width="' . $width . '"' : '';
1453
							$height = !empty($height) ? ' height="' . $height . '"' : '';
1454
1455
							$returnContext .= '<div class="videocontainer"><div><video controls preload="none" src="'. $currentAttachment['href'] . '" playsinline' . $width . $height . ' style="object-fit:contain;"><a href="' . $currentAttachment['href'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . '</a></video></div></div>' . (!empty($data) && $data != $currentAttachment['name'] ? '<div class="smalltext">' . $data . '</div>' : '');
1456
						}
1457
						// Audio.
1458
						elseif (strpos($currentAttachment['mime_type'], 'audio/') === 0)
1459
						{
1460
							$width = 'max-width:100%; width: ' . (!empty($width) ? $width : '400') . 'px;';
1461
							$height = !empty($height) ? 'height: ' . $height . 'px;' : '';
1462
1463
							$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>';
1464
						}
1465
						// Anything else.
1466
						else
1467
						{
1468
							$width = !empty($width) ? ' width="' . $width . '"' : '';
1469
							$height = !empty($height) ? ' height="' . $height . '"' : '';
1470
1471
							$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>';
1472
						}
1473
					}
1474
1475
					// No image. Show a link.
1476
					else
1477
						$returnContext .= '<a href="' . $currentAttachment['href'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . '</a>';
1478
1479
					// Gotta append what we just did.
1480
					$data = $returnContext;
1481
				},
1482
			),
1483
			array(
1484
				'tag' => 'b',
1485
				'before' => '<b>',
1486
				'after' => '</b>',
1487
			),
1488
			// Legacy (equivalent to [ltr] or [rtl])
1489
			array(
1490
				'tag' => 'bdo',
1491
				'type' => 'unparsed_equals',
1492
				'before' => '<bdo dir="$1">',
1493
				'after' => '</bdo>',
1494
				'test' => '(rtl|ltr)\]',
1495
				'block_level' => true,
1496
			),
1497
			// Legacy (alias of [color=black])
1498
			array(
1499
				'tag' => 'black',
1500
				'before' => '<span style="color: black;" class="bbc_color">',
1501
				'after' => '</span>',
1502
			),
1503
			// Legacy (alias of [color=blue])
1504
			array(
1505
				'tag' => 'blue',
1506
				'before' => '<span style="color: blue;" class="bbc_color">',
1507
				'after' => '</span>',
1508
			),
1509
			array(
1510
				'tag' => 'br',
1511
				'type' => 'closed',
1512
				'content' => '<br>',
1513
			),
1514
			array(
1515
				'tag' => 'center',
1516
				'before' => '<div class="centertext">',
1517
				'after' => '</div>',
1518
				'block_level' => true,
1519
			),
1520
			array(
1521
				'tag' => 'code',
1522
				'type' => 'unparsed_content',
1523
				'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>',
1524
				// @todo Maybe this can be simplified?
1525
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1526
				{
1527
					if (!isset($disabled['code']))
1528
					{
1529
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
1530
1531
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
0 ignored issues
show
Bug introduced by
It seems like $php_parts can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

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

1531
						for ($php_i = 0, $php_n = count(/** @scrutinizer ignore-type */ $php_parts); $php_i < $php_n; $php_i++)
Loading history...
1532
						{
1533
							// Do PHP code coloring?
1534
							if ($php_parts[$php_i] != '&lt;?php')
1535
								continue;
1536
1537
							$php_string = '';
1538
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1539
							{
1540
								$php_string .= $php_parts[$php_i];
1541
								$php_parts[$php_i++] = '';
1542
							}
1543
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1544
						}
1545
1546
						// Fix the PHP code stuff...
1547
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
0 ignored issues
show
Bug introduced by
It seems like $php_parts can also be of type false; however, parameter $pieces of implode() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

1547
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', /** @scrutinizer ignore-type */ $php_parts));
Loading history...
1548
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1549
1550
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1551
						if (!empty($context['browser']['is_opera']))
1552
							$data .= '&nbsp;';
1553
					}
1554
				},
1555
				'block_level' => true,
1556
			),
1557
			array(
1558
				'tag' => 'code',
1559
				'type' => 'unparsed_equals_content',
1560
				'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>',
1561
				// @todo Maybe this can be simplified?
1562
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1563
				{
1564
					if (!isset($disabled['code']))
1565
					{
1566
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1567
1568
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
0 ignored issues
show
Bug introduced by
It seems like $php_parts can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

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

1568
						for ($php_i = 0, $php_n = count(/** @scrutinizer ignore-type */ $php_parts); $php_i < $php_n; $php_i++)
Loading history...
1569
						{
1570
							// Do PHP code coloring?
1571
							if ($php_parts[$php_i] != '&lt;?php')
1572
								continue;
1573
1574
							$php_string = '';
1575
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1576
							{
1577
								$php_string .= $php_parts[$php_i];
1578
								$php_parts[$php_i++] = '';
1579
							}
1580
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1581
						}
1582
1583
						// Fix the PHP code stuff...
1584
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
0 ignored issues
show
Bug introduced by
It seems like $php_parts can also be of type false; however, parameter $pieces of implode() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

1584
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', /** @scrutinizer ignore-type */ $php_parts));
Loading history...
1585
						$data[0] = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data[0]);
1586
1587
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1588
						if (!empty($context['browser']['is_opera']))
1589
							$data[0] .= '&nbsp;';
1590
					}
1591
				},
1592
				'block_level' => true,
1593
			),
1594
			array(
1595
				'tag' => 'color',
1596
				'type' => 'unparsed_equals',
1597
				'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]?)\))\]',
1598
				'before' => '<span style="color: $1;" class="bbc_color">',
1599
				'after' => '</span>',
1600
			),
1601
			array(
1602
				'tag' => 'email',
1603
				'type' => 'unparsed_content',
1604
				'content' => '<a href="mailto:$1" class="bbc_email">$1</a>',
1605
				// @todo Should this respect guest_hideContacts?
1606
				'validate' => function(&$tag, &$data, $disabled)
1607
				{
1608
					$data = strtr($data, array('<br>' => ''));
1609
				},
1610
			),
1611
			array(
1612
				'tag' => 'email',
1613
				'type' => 'unparsed_equals',
1614
				'before' => '<a href="mailto:$1" class="bbc_email">',
1615
				'after' => '</a>',
1616
				// @todo Should this respect guest_hideContacts?
1617
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1618
				'disabled_after' => ' ($1)',
1619
			),
1620
			// Legacy (and just a link even when not disabled)
1621
			array(
1622
				'tag' => 'flash',
1623
				'type' => 'unparsed_commas_content',
1624
				'test' => '\d+,\d+\]',
1625
				'content' => '<a href="$1" target="_blank" rel="noopener">$1</a>',
1626
				'validate' => function (&$tag, &$data, $disabled)
1627
				{
1628
					$scheme = parse_url($data[0], PHP_URL_SCHEME);
1629
					if (empty($scheme))
1630
						$data[0] = '//' . ltrim($data[0], ':/');
1631
				},
1632
			),
1633
			array(
1634
				'tag' => 'float',
1635
				'type' => 'unparsed_equals',
1636
				'test' => '(left|right)(\s+max=\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)?\]',
1637
				'before' => '<div $1>',
1638
				'after' => '</div>',
1639
				'validate' => function(&$tag, &$data, $disabled)
1640
				{
1641
					$class = 'class="bbc_float float' . (strpos($data, 'left') === 0 ? 'left' : 'right') . '"';
1642
1643
					if (preg_match('~\bmax=(\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)~', $data, $matches))
1644
						$css = ' style="max-width:' . $matches[1] . (is_numeric($matches[1]) ? 'px' : '') . '"';
1645
					else
1646
						$css = '';
1647
1648
					$data = $class . $css;
1649
				},
1650
				'trim' => 'outside',
1651
				'block_level' => true,
1652
			),
1653
			// Legacy (alias of [url] with an FTP URL)
1654
			array(
1655
				'tag' => 'ftp',
1656
				'type' => 'unparsed_content',
1657
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
1658
				'validate' => function(&$tag, &$data, $disabled)
1659
				{
1660
					$data = strtr($data, array('<br>' => ''));
1661
					$scheme = parse_url($data, PHP_URL_SCHEME);
1662
					if (empty($scheme))
1663
						$data = 'ftp://' . ltrim($data, ':/');
1664
				},
1665
			),
1666
			// Legacy (alias of [url] with an FTP URL)
1667
			array(
1668
				'tag' => 'ftp',
1669
				'type' => 'unparsed_equals',
1670
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
1671
				'after' => '</a>',
1672
				'validate' => function(&$tag, &$data, $disabled)
1673
				{
1674
					$scheme = parse_url($data, PHP_URL_SCHEME);
1675
					if (empty($scheme))
1676
						$data = 'ftp://' . ltrim($data, ':/');
1677
				},
1678
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1679
				'disabled_after' => ' ($1)',
1680
			),
1681
			array(
1682
				'tag' => 'font',
1683
				'type' => 'unparsed_equals',
1684
				'test' => '[A-Za-z0-9_,\-\s]+?\]',
1685
				'before' => '<span style="font-family: $1;" class="bbc_font">',
1686
				'after' => '</span>',
1687
			),
1688
			// Legacy (one of those things that should not be done)
1689
			array(
1690
				'tag' => 'glow',
1691
				'type' => 'unparsed_commas',
1692
				'test' => '[#0-9a-zA-Z\-]{3,12},([012]\d{1,2}|\d{1,2})(,[^]]+)?\]',
1693
				'before' => '<span style="text-shadow: $1 1px 1px 1px">',
1694
				'after' => '</span>',
1695
			),
1696
			// Legacy (alias of [color=green])
1697
			array(
1698
				'tag' => 'green',
1699
				'before' => '<span style="color: green;" class="bbc_color">',
1700
				'after' => '</span>',
1701
			),
1702
			array(
1703
				'tag' => 'html',
1704
				'type' => 'unparsed_content',
1705
				'content' => '<div>$1</div>',
1706
				'block_level' => true,
1707
				'disabled_content' => '$1',
1708
			),
1709
			array(
1710
				'tag' => 'hr',
1711
				'type' => 'closed',
1712
				'content' => '<hr>',
1713
				'block_level' => true,
1714
			),
1715
			array(
1716
				'tag' => 'i',
1717
				'before' => '<i>',
1718
				'after' => '</i>',
1719
			),
1720
			array(
1721
				'tag' => 'img',
1722
				'type' => 'unparsed_content',
1723
				'parameters' => array(
1724
					'alt' => array('optional' => true),
1725
					'title' => array('optional' => true),
1726
					'width' => array('optional' => true, 'value' => ' width="$1"', 'match' => '(\d+)'),
1727
					'height' => array('optional' => true, 'value' => ' height="$1"', 'match' => '(\d+)'),
1728
				),
1729
				'content' => '<img src="$1" alt="{alt}" title="{title}"{width}{height} class="bbc_img resized">',
1730
				'validate' => function(&$tag, &$data, $disabled)
1731
				{
1732
					$data = strtr($data, array('<br>' => ''));
1733
1734
					if (parse_url($data, PHP_URL_SCHEME) === null)
1735
						$data = '//' . ltrim($data, ':/');
1736
					else
1737
						$data = get_proxied_url($data);
1738
				},
1739
				'disabled_content' => '($1)',
1740
			),
1741
			array(
1742
				'tag' => 'img',
1743
				'type' => 'unparsed_content',
1744
				'content' => '<img src="$1" alt="" class="bbc_img">',
1745
				'validate' => function(&$tag, &$data, $disabled)
1746
				{
1747
					$data = strtr($data, array('<br>' => ''));
1748
1749
					if (parse_url($data, PHP_URL_SCHEME) === null)
1750
						$data = '//' . ltrim($data, ':/');
1751
					else
1752
						$data = get_proxied_url($data);
1753
				},
1754
				'disabled_content' => '($1)',
1755
			),
1756
			array(
1757
				'tag' => 'iurl',
1758
				'type' => 'unparsed_content',
1759
				'content' => '<a href="$1" class="bbc_link">$1</a>',
1760
				'validate' => function(&$tag, &$data, $disabled)
1761
				{
1762
					$data = strtr($data, array('<br>' => ''));
1763
					$scheme = parse_url($data, PHP_URL_SCHEME);
1764
					if (empty($scheme))
1765
						$data = '//' . ltrim($data, ':/');
1766
				},
1767
			),
1768
			array(
1769
				'tag' => 'iurl',
1770
				'type' => 'unparsed_equals',
1771
				'quoted' => 'optional',
1772
				'before' => '<a href="$1" class="bbc_link">',
1773
				'after' => '</a>',
1774
				'validate' => function(&$tag, &$data, $disabled)
1775
				{
1776
					if (substr($data, 0, 1) == '#')
1777
						$data = '#post_' . substr($data, 1);
1778
					else
1779
					{
1780
						$scheme = parse_url($data, PHP_URL_SCHEME);
1781
						if (empty($scheme))
1782
							$data = '//' . ltrim($data, ':/');
1783
					}
1784
				},
1785
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1786
				'disabled_after' => ' ($1)',
1787
			),
1788
			array(
1789
				'tag' => 'justify',
1790
				'before' => '<div style="text-align: justify;">',
1791
				'after' => '</div>',
1792
				'block_level' => true,
1793
			),
1794
			array(
1795
				'tag' => 'left',
1796
				'before' => '<div style="text-align: left;">',
1797
				'after' => '</div>',
1798
				'block_level' => true,
1799
			),
1800
			array(
1801
				'tag' => 'li',
1802
				'before' => '<li>',
1803
				'after' => '</li>',
1804
				'trim' => 'outside',
1805
				'require_parents' => array('list'),
1806
				'block_level' => true,
1807
				'disabled_before' => '',
1808
				'disabled_after' => '<br>',
1809
			),
1810
			array(
1811
				'tag' => 'list',
1812
				'before' => '<ul class="bbc_list">',
1813
				'after' => '</ul>',
1814
				'trim' => 'inside',
1815
				'require_children' => array('li', 'list'),
1816
				'block_level' => true,
1817
			),
1818
			array(
1819
				'tag' => 'list',
1820
				'parameters' => array(
1821
					'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)'),
1822
				),
1823
				'before' => '<ul class="bbc_list" style="list-style-type: {type};">',
1824
				'after' => '</ul>',
1825
				'trim' => 'inside',
1826
				'require_children' => array('li'),
1827
				'block_level' => true,
1828
			),
1829
			array(
1830
				'tag' => 'ltr',
1831
				'before' => '<bdo dir="ltr">',
1832
				'after' => '</bdo>',
1833
				'block_level' => true,
1834
			),
1835
			array(
1836
				'tag' => 'me',
1837
				'type' => 'unparsed_equals',
1838
				'before' => '<div class="meaction">* $1 ',
1839
				'after' => '</div>',
1840
				'quoted' => 'optional',
1841
				'block_level' => true,
1842
				'disabled_before' => '/me ',
1843
				'disabled_after' => '<br>',
1844
			),
1845
			array(
1846
				'tag' => 'member',
1847
				'type' => 'unparsed_equals',
1848
				'before' => '<a href="' . $scripturl . '?action=profile;u=$1" class="mention" data-mention="$1">@',
1849
				'after' => '</a>',
1850
			),
1851
			// Legacy (horrible memories of the 1990s)
1852
			array(
1853
				'tag' => 'move',
1854
				'before' => '<marquee>',
1855
				'after' => '</marquee>',
1856
				'block_level' => true,
1857
				'disallow_children' => array('move'),
1858
			),
1859
			array(
1860
				'tag' => 'nobbc',
1861
				'type' => 'unparsed_content',
1862
				'content' => '$1',
1863
			),
1864
			array(
1865
				'tag' => 'php',
1866
				'type' => 'unparsed_content',
1867
				'content' => '<span class="phpcode">$1</span>',
1868
				'validate' => isset($disabled['php']) ? null : function(&$tag, &$data, $disabled)
1869
				{
1870
					if (!isset($disabled['php']))
1871
					{
1872
						$add_begin = substr(trim($data), 0, 5) != '&lt;?';
1873
						$data = highlight_php_code($add_begin ? '&lt;?php ' . $data . '?&gt;' : $data);
1874
						if ($add_begin)
1875
							$data = preg_replace(array('~^(.+?)&lt;\?.{0,40}?php(?:&nbsp;|\s)~', '~\?&gt;((?:</(font|span)>)*)$~'), '$1', $data, 2);
1876
					}
1877
				},
1878
				'block_level' => false,
1879
				'disabled_content' => '$1',
1880
			),
1881
			array(
1882
				'tag' => 'pre',
1883
				'before' => '<pre>',
1884
				'after' => '</pre>',
1885
			),
1886
			array(
1887
				'tag' => 'quote',
1888
				'before' => '<blockquote><cite>' . $txt['quote'] . '</cite>',
1889
				'after' => '</blockquote>',
1890
				'trim' => 'both',
1891
				'block_level' => true,
1892
			),
1893
			array(
1894
				'tag' => 'quote',
1895
				'parameters' => array(
1896
					'author' => array('match' => '(.{1,192}?)', 'quoted' => true),
1897
				),
1898
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1899
				'after' => '</blockquote>',
1900
				'trim' => 'both',
1901
				'block_level' => true,
1902
			),
1903
			array(
1904
				'tag' => 'quote',
1905
				'type' => 'parsed_equals',
1906
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': $1</cite>',
1907
				'after' => '</blockquote>',
1908
				'trim' => 'both',
1909
				'quoted' => 'optional',
1910
				// Don't allow everything to be embedded with the author name.
1911
				'parsed_tags_allowed' => array('url', 'iurl', 'ftp'),
1912
				'block_level' => true,
1913
			),
1914
			array(
1915
				'tag' => 'quote',
1916
				'parameters' => array(
1917
					'author' => array('match' => '([^<>]{1,192}?)'),
1918
					'link' => array('match' => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|msg=\d+?|action=profile;u=\d+)'),
1919
					'date' => array('match' => '(\d+)', 'validate' => 'timeformat'),
1920
				),
1921
				'before' => '<blockquote><cite><a href="' . $scripturl . '?{link}">' . $txt['quote_from'] . ': {author} ' . $txt['search_on'] . ' {date}</a></cite>',
1922
				'after' => '</blockquote>',
1923
				'trim' => 'both',
1924
				'block_level' => true,
1925
			),
1926
			array(
1927
				'tag' => 'quote',
1928
				'parameters' => array(
1929
					'author' => array('match' => '(.{1,192}?)'),
1930
				),
1931
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1932
				'after' => '</blockquote>',
1933
				'trim' => 'both',
1934
				'block_level' => true,
1935
			),
1936
			// Legacy (alias of [color=red])
1937
			array(
1938
				'tag' => 'red',
1939
				'before' => '<span style="color: red;" class="bbc_color">',
1940
				'after' => '</span>',
1941
			),
1942
			array(
1943
				'tag' => 'right',
1944
				'before' => '<div style="text-align: right;">',
1945
				'after' => '</div>',
1946
				'block_level' => true,
1947
			),
1948
			array(
1949
				'tag' => 'rtl',
1950
				'before' => '<bdo dir="rtl">',
1951
				'after' => '</bdo>',
1952
				'block_level' => true,
1953
			),
1954
			array(
1955
				'tag' => 's',
1956
				'before' => '<s>',
1957
				'after' => '</s>',
1958
			),
1959
			// Legacy (never a good idea)
1960
			array(
1961
				'tag' => 'shadow',
1962
				'type' => 'unparsed_commas',
1963
				'test' => '[#0-9a-zA-Z\-]{3,12},(left|right|top|bottom|[0123]\d{0,2})\]',
1964
				'before' => '<span style="text-shadow: $1 $2">',
1965
				'after' => '</span>',
1966
				'validate' => function(&$tag, &$data, $disabled)
1967
				{
1968
1969
					if ($data[1] == 'top' || (is_numeric($data[1]) && $data[1] < 50))
1970
						$data[1] = '0 -2px 1px';
1971
1972
					elseif ($data[1] == 'right' || (is_numeric($data[1]) && $data[1] < 100))
1973
						$data[1] = '2px 0 1px';
1974
1975
					elseif ($data[1] == 'bottom' || (is_numeric($data[1]) && $data[1] < 190))
1976
						$data[1] = '0 2px 1px';
1977
1978
					elseif ($data[1] == 'left' || (is_numeric($data[1]) && $data[1] < 280))
1979
						$data[1] = '-2px 0 1px';
1980
1981
					else
1982
						$data[1] = '1px 1px 1px';
1983
				},
1984
			),
1985
			array(
1986
				'tag' => 'size',
1987
				'type' => 'unparsed_equals',
1988
				'test' => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)\]',
1989
				'before' => '<span style="font-size: $1;" class="bbc_size">',
1990
				'after' => '</span>',
1991
			),
1992
			array(
1993
				'tag' => 'size',
1994
				'type' => 'unparsed_equals',
1995
				'test' => '[1-7]\]',
1996
				'before' => '<span style="font-size: $1;" class="bbc_size">',
1997
				'after' => '</span>',
1998
				'validate' => function(&$tag, &$data, $disabled)
1999
				{
2000
					$sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95);
2001
					$data = $sizes[$data] . 'em';
2002
				},
2003
			),
2004
			array(
2005
				'tag' => 'sub',
2006
				'before' => '<sub>',
2007
				'after' => '</sub>',
2008
			),
2009
			array(
2010
				'tag' => 'sup',
2011
				'before' => '<sup>',
2012
				'after' => '</sup>',
2013
			),
2014
			array(
2015
				'tag' => 'table',
2016
				'before' => '<table class="bbc_table">',
2017
				'after' => '</table>',
2018
				'trim' => 'inside',
2019
				'require_children' => array('tr'),
2020
				'block_level' => true,
2021
			),
2022
			array(
2023
				'tag' => 'td',
2024
				'before' => '<td>',
2025
				'after' => '</td>',
2026
				'require_parents' => array('tr'),
2027
				'trim' => 'outside',
2028
				'block_level' => true,
2029
				'disabled_before' => '',
2030
				'disabled_after' => '',
2031
			),
2032
			array(
2033
				'tag' => 'time',
2034
				'type' => 'unparsed_content',
2035
				'content' => '$1',
2036
				'validate' => function(&$tag, &$data, $disabled)
2037
				{
2038
					if (is_numeric($data))
2039
						$data = timeformat($data);
2040
					else
2041
						$tag['content'] = '[time]$1[/time]';
2042
				},
2043
			),
2044
			array(
2045
				'tag' => 'tr',
2046
				'before' => '<tr>',
2047
				'after' => '</tr>',
2048
				'require_parents' => array('table'),
2049
				'require_children' => array('td'),
2050
				'trim' => 'both',
2051
				'block_level' => true,
2052
				'disabled_before' => '',
2053
				'disabled_after' => '',
2054
			),
2055
			// Legacy (the <tt> element is dead)
2056
			array(
2057
				'tag' => 'tt',
2058
				'before' => '<span class="monospace">',
2059
				'after' => '</span>',
2060
			),
2061
			array(
2062
				'tag' => 'u',
2063
				'before' => '<u>',
2064
				'after' => '</u>',
2065
			),
2066
			array(
2067
				'tag' => 'url',
2068
				'type' => 'unparsed_content',
2069
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
2070
				'validate' => function(&$tag, &$data, $disabled)
2071
				{
2072
					$data = strtr($data, array('<br>' => ''));
2073
					$scheme = parse_url($data, PHP_URL_SCHEME);
2074
					if (empty($scheme))
2075
						$data = '//' . ltrim($data, ':/');
2076
				},
2077
			),
2078
			array(
2079
				'tag' => 'url',
2080
				'type' => 'unparsed_equals',
2081
				'quoted' => 'optional',
2082
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
2083
				'after' => '</a>',
2084
				'validate' => function(&$tag, &$data, $disabled)
2085
				{
2086
					$scheme = parse_url($data, PHP_URL_SCHEME);
2087
					if (empty($scheme))
2088
						$data = '//' . ltrim($data, ':/');
2089
				},
2090
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
2091
				'disabled_after' => ' ($1)',
2092
			),
2093
			// Legacy (alias of [color=white])
2094
			array(
2095
				'tag' => 'white',
2096
				'before' => '<span style="color: white;" class="bbc_color">',
2097
				'after' => '</span>',
2098
			),
2099
			array(
2100
				'tag' => 'youtube',
2101
				'type' => 'unparsed_content',
2102
				'content' => '<div class="videocontainer"><div><iframe frameborder="0" src="https://www.youtube.com/embed/$1?origin=' . $hosturl . '&wmode=opaque" data-youtube-id="$1" allowfullscreen></iframe></div></div>',
2103
				'disabled_content' => '<a href="https://www.youtube.com/watch?v=$1" target="_blank" rel="noopener">https://www.youtube.com/watch?v=$1</a>',
2104
				'block_level' => true,
2105
			),
2106
		);
2107
2108
		// Inside these tags autolink is not recommendable.
2109
		$no_autolink_tags = array(
2110
			'url',
2111
			'iurl',
2112
			'email',
2113
			'img',
2114
			'html',
2115
		);
2116
2117
		// Let mods add new BBC without hassle.
2118
		call_integration_hook('integrate_bbc_codes', array(&$codes, &$no_autolink_tags));
2119
2120
		// This is mainly for the bbc manager, so it's easy to add tags above.  Custom BBC should be added above this line.
2121
		if ($message === false)
0 ignored issues
show
introduced by
The condition $message === false is always false.
Loading history...
2122
		{
2123
			usort($codes, function($a, $b)
2124
			{
2125
				return strcmp($a['tag'], $b['tag']);
2126
			});
2127
			return $codes;
2128
		}
2129
2130
		// So the parser won't skip them.
2131
		$itemcodes = array(
2132
			'*' => 'disc',
2133
			'@' => 'disc',
2134
			'+' => 'square',
2135
			'x' => 'square',
2136
			'#' => 'square',
2137
			'o' => 'circle',
2138
			'O' => 'circle',
2139
			'0' => 'circle',
2140
		);
2141
		if (!isset($disabled['li']) && !isset($disabled['list']))
2142
		{
2143
			foreach ($itemcodes as $c => $dummy)
2144
				$bbc_codes[$c] = array();
2145
		}
2146
2147
		// Shhhh!
2148
		if (!isset($disabled['color']))
2149
		{
2150
			$codes[] = array(
2151
				'tag' => 'chrissy',
2152
				'before' => '<span style="color: #cc0099;">',
2153
				'after' => ' :-*</span>',
2154
			);
2155
			$codes[] = array(
2156
				'tag' => 'kissy',
2157
				'before' => '<span style="color: #cc0099;">',
2158
				'after' => ' :-*</span>',
2159
			);
2160
		}
2161
		$codes[] = array(
2162
			'tag' => 'cowsay',
2163
			'parameters' => array(
2164
				'e' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => 'oo', 'validate' => function ($eyes) use ($smcFunc)
2165
					{
2166
						static $css_added;
2167
2168
						if (empty($css_added))
2169
						{
2170
							$css = base64_decode('cHJlW2RhdGEtZV1bZGF0YS10XXt3aGl0ZS1zcGFjZTpwcmUtd3JhcDtsaW5lLWhlaWdodDppbml0aWFsO31wcmVbZGF0YS1lXVtkYXRhLXRdID4gZGl2e2Rpc3BsYXk6dGFibGU7Ym9yZGVyOjFweCBzb2xpZDtib3JkZXItcmFkaXVzOjAuNWVtO3BhZGRpbmc6MWNoO21heC13aWR0aDo4MGNoO21pbi13aWR0aDoxMmNoO31wcmVbZGF0YS1lXVtkYXRhLXRdOjphZnRlcntkaXNwbGF5OmlubGluZS1ibG9jazttYXJnaW4tbGVmdDo4Y2g7bWluLXdpZHRoOjIwY2g7ZGlyZWN0aW9uOmx0cjtjb250ZW50OidcNUMgICBeX19eXEEgIFw1QyAgKCcgYXR0cihkYXRhLWUpICcpXDVDX19fX19fX1xBICAgIChfXylcNUMgICAgICAgIClcNUMvXDVDXEEgICAgICcgYXR0cihkYXRhLXQpICcgfHwtLS0tdyB8XEEgICAgICAgIHx8ICAgICB8fCc7fQ==');
2171
2172
							addInlineJavaScript('
2173
								$("head").append("<style>" + ' . JavaScriptEscape($css) . ' + "</style>");', true);
2174
2175
							$css_added = true;
2176
						}
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></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)
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $temp = cache_get_data($cache_key, 240) of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
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
			$disabled['img'] = true;
2246
2247
		// Maybe some custom BBC need to be disabled for printing.
2248
		call_integration_hook('integrate_bbc_print', array(&$disabled));
2249
	}
2250
2251
	$open_tags = array();
2252
	$message = strtr($message, array("\n" => '<br>'));
2253
2254
	if (!empty($parse_tags))
2255
	{
2256
		$real_alltags_regex = $alltags_regex;
2257
		$alltags_regex = '';
2258
	}
2259
	if (empty($alltags_regex))
2260
	{
2261
		$alltags = array();
2262
		foreach ($bbc_codes as $section)
2263
		{
2264
			foreach ($section as $code)
2265
				$alltags[] = $code['tag'];
2266
		}
2267
		$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_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

2267
		$alltags_regex = '(?' . '>\b' . build_regex(array_unique($alltags)) . '\b|' . /** @scrutinizer ignore-type */ build_regex(array_keys($itemcodes)) . ')';
Loading history...
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

2267
		$alltags_regex = '(?' . '>\b' . /** @scrutinizer ignore-type */ build_regex(array_unique($alltags)) . '\b|' . build_regex(array_keys($itemcodes)) . ')';
Loading history...
2268
	}
2269
2270
	$pos = -1;
2271
	while ($pos !== false)
2272
	{
2273
		$last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos;
2274
		preg_match('~\[/?(?=' . $alltags_regex . ')~i', $message, $matches, PREG_OFFSET_CAPTURE, $pos + 1);
2275
		$pos = isset($matches[0][1]) ? $matches[0][1] : false;
2276
2277
		// Failsafe.
2278
		if ($pos === false || $last_pos > $pos)
2279
			$pos = strlen($message) + 1;
2280
2281
		// Can't have a one letter smiley, URL, or email! (Sorry.)
2282
		if ($last_pos < $pos - 1)
2283
		{
2284
			// Make sure the $last_pos is not negative.
2285
			$last_pos = max($last_pos, 0);
2286
2287
			// Pick a block of data to do some raw fixing on.
2288
			$data = substr($message, $last_pos, $pos - $last_pos);
2289
2290
			$placeholders = array();
2291
			$placeholders_counter = 0;
2292
2293
			// Take care of some HTML!
2294
			if (!empty($modSettings['enablePostHTML']) && strpos($data, '&lt;') !== false)
2295
			{
2296
				$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);
2297
2298
				// <br> should be empty.
2299
				$empty_tags = array('br', 'hr');
2300
				foreach ($empty_tags as $tag)
2301
					$data = str_replace(array('&lt;' . $tag . '&gt;', '&lt;' . $tag . '/&gt;', '&lt;' . $tag . ' /&gt;'), '<' . $tag . '>', $data);
2302
2303
				// b, u, i, s, pre... basic tags.
2304
				$closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote', 'strong');
2305
				foreach ($closable_tags as $tag)
2306
				{
2307
					$diff = substr_count($data, '&lt;' . $tag . '&gt;') - substr_count($data, '&lt;/' . $tag . '&gt;');
2308
					$data = strtr($data, array('&lt;' . $tag . '&gt;' => '<' . $tag . '>', '&lt;/' . $tag . '&gt;' => '</' . $tag . '>'));
2309
2310
					if ($diff > 0)
2311
						$data = substr($data, 0, -1) . str_repeat('</' . $tag . '>', $diff) . substr($data, -1);
2312
				}
2313
2314
				// Do <img ...> - with security... action= -> action-.
2315
				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);
2316
				if (!empty($matches[0]))
2317
				{
2318
					$replaces = array();
2319
					foreach ($matches[2] as $match => $imgtag)
2320
					{
2321
						$alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^&quot;|&quot;$~', '', $matches[3][$match]);
2322
2323
						// Remove action= from the URL - no funny business, now.
2324
						// @todo Testing this preg_match seems pointless
2325
						if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) != 0)
2326
							$imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag);
2327
2328
						$placeholder = '<placeholder ' . ++$placeholders_counter . '>';
2329
						$placeholders[$placeholder] = '[img' . $alt . ']' . $imgtag . '[/img]';
2330
2331
						$replaces[$matches[0][$match]] = $placeholder;
2332
					}
2333
2334
					$data = strtr($data, $replaces);
2335
				}
2336
			}
2337
2338
			if (!empty($modSettings['autoLinkUrls']))
2339
			{
2340
				// Are we inside tags that should be auto linked?
2341
				$no_autolink_area = false;
2342
				if (!empty($open_tags))
2343
				{
2344
					foreach ($open_tags as $open_tag)
2345
						if (in_array($open_tag['tag'], $no_autolink_tags))
2346
							$no_autolink_area = true;
2347
				}
2348
2349
				// Don't go backwards.
2350
				// @todo Don't think is the real solution....
2351
				$lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0;
2352
				if ($pos < $lastAutoPos)
2353
					$no_autolink_area = true;
2354
				$lastAutoPos = $pos;
2355
2356
				if (!$no_autolink_area)
2357
				{
2358
					// An &nbsp; right after a URL can break the autolinker
2359
					if (strpos($data, '&nbsp;') !== false)
2360
					{
2361
						$placeholders['<placeholder non-breaking-space>'] = '&nbsp;';
2362
						$data = strtr($data, array('&nbsp;' => '<placeholder non-breaking-space>'));
2363
					}
2364
2365
					// Parse any URLs
2366
					if (!isset($disabled['url']) && strpos($data, '[url') === false)
2367
					{
2368
						// For efficiency, first define the TLD regex in a PCRE subroutine
2369
						$url_regex = '(?(DEFINE)(?<tlds>' . $modSettings['tld_regex'] . '))';
2370
2371
						// Now build the rest of the regex
2372
						$url_regex .=
2373
						// 1. IRI scheme and domain components
2374
						'(?:' .
2375
							// 1a. IRIs with a scheme, or at least an opening "//"
2376
							'(?:' .
2377
2378
								// URI scheme (or lack thereof for schemeless URLs)
2379
								'(?:' .
2380
									// URL scheme and colon
2381
									'\b[a-z][\w\-]+:' .
2382
									// or
2383
									'|' .
2384
									// A boundary followed by two slashes for schemeless URLs
2385
									'(?<=^|\W)(?=//)' .
2386
								')' .
2387
2388
								// IRI "authority" chunk
2389
								'(?:' .
2390
									// 2 slashes for IRIs with an "authority"
2391
									'//' .
2392
									// then a domain name
2393
									'(?:' .
2394
										// Either the reserved "localhost" domain name
2395
										'localhost' .
2396
										// or
2397
										'|' .
2398
										// a run of IRI characters, a dot, and a TLD
2399
										'[\p{L}\p{M}\p{N}\-.:@]+\.(?P>tlds)' .
2400
									')' .
2401
									// followed by a non-domain character or end of line
2402
									'(?=[^\p{L}\p{N}\-.]|$)' .
2403
2404
									// or, if no "authority" per se (e.g. "mailto:" URLs)...
2405
									'|' .
2406
2407
									// a run of IRI characters
2408
									'[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.:@]+[\p{L}\p{M}\p{N}]' .
2409
									// and then a dot and a closing IRI label
2410
									'\.[\p{L}\p{M}\p{N}\-]+' .
2411
								')' .
2412
							')' .
2413
2414
							// Or
2415
							'|' .
2416
2417
							// 1b. Naked domains (e.g. "example.com" in "Go to example.com for an example.")
2418
							'(?:' .
2419
								// Preceded by start of line or a non-domain character
2420
								'(?<=^|[^\p{L}\p{M}\p{N}\-:@])' .
2421
								// A run of Unicode domain name characters (excluding [:@])
2422
								'[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.]+[\p{L}\p{M}\p{N}]' .
2423
								// and then a dot and a valid TLD
2424
								'\.(?P>tlds)' .
2425
								// Followed by either:
2426
								'(?=' .
2427
									// end of line or a non-domain character (excluding [.:@])
2428
									'$|[^\p{L}\p{N}\-]' .
2429
									// or
2430
									'|' .
2431
									// a dot followed by end of line or a non-domain character (excluding [.:@])
2432
									'\.(?=$|[^\p{L}\p{N}\-])' .
2433
								')' .
2434
							')' .
2435
						')' .
2436
2437
						// 2. IRI path, query, and fragment components (if present)
2438
						'(?:' .
2439
2440
							// If any of these parts exist, must start with a single "/"
2441
							'/' .
2442
2443
							// And then optionally:
2444
							'(?:' .
2445
								// One or more of:
2446
								'(?:' .
2447
									// a run of non-space, non-()<>
2448
									'[^\s()<>]+' .
2449
									// or
2450
									'|' .
2451
									// balanced parentheses, up to 2 levels
2452
									'\(([^\s()<>]+|(\([^\s()<>]+\)))*\)' .
2453
								')+' .
2454
								// Ending with:
2455
								'(?:' .
2456
									// balanced parentheses, up to 2 levels
2457
									'\(([^\s()<>]+|(\([^\s()<>]+\)))*\)' .
2458
									// or
2459
									'|' .
2460
									// not a space or one of these punctuation characters
2461
									'[^\s`!()\[\]{};:\'".,<>?«»“”‘’/]' .
2462
									// or
2463
									'|' .
2464
									// a trailing slash (but not two in a row)
2465
									'(?<!/)/' .
2466
								')' .
2467
							')?' .
2468
						')?';
2469
2470
						$data = preg_replace_callback('~' . $url_regex . '~i' . ($context['utf8'] ? 'u' : ''), function($matches)
2471
						{
2472
							$url = array_shift($matches);
2473
2474
							// If this isn't a clean URL, bail out
2475
							if ($url != sanitize_iri($url))
2476
								return $url;
2477
2478
							$scheme = parse_url($url, PHP_URL_SCHEME);
2479
2480
							if ($scheme == 'mailto')
2481
							{
2482
								$email_address = str_replace('mailto:', '', $url);
2483
								if (!isset($disabled['email']) && filter_var($email_address, FILTER_VALIDATE_EMAIL) !== false)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $disabled seems to never exist and therefore isset should always be false.
Loading history...
2484
									return '[email=' . $email_address . ']' . $url . '[/email]';
2485
								else
2486
									return $url;
2487
							}
2488
2489
							// Are we linking a schemeless URL or naked domain name (e.g. "example.com")?
2490
							if (empty($scheme))
2491
								$fullUrl = '//' . ltrim($url, ':/');
2492
							else
2493
								$fullUrl = $url;
2494
2495
							// Make sure that $fullUrl really is valid
2496
							if (validate_iri((strpos($fullUrl, '//') === 0 ? 'http:' : '') . $fullUrl) === false)
2497
								return $url;
2498
2499
							return '[url=&quot;' . str_replace(array('[', ']'), array('&#91;', '&#93;'), $fullUrl) . '&quot;]' . $url . '[/url]';
2500
						}, $data);
2501
					}
2502
2503
					// Next, emails...  Must be careful not to step on enablePostHTML logic above...
2504
					if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false && stripos($data, 'mailto:') === false)
2505
					{
2506
						$email_regex = '
2507
						# Preceded by a non-domain character or start of line
2508
						(?<=^|[^\p{L}\p{M}\p{N}\-\.])
2509
2510
						# An email address
2511
						[\p{L}\p{M}\p{N}_\-.]{1,80}
2512
						@
2513
						[\p{L}\p{M}\p{N}\-.]+
2514
						\.
2515
						' . $modSettings['tld_regex'] . '
2516
2517
						# Followed by either:
2518
						(?=
2519
							# end of line or a non-domain character (excluding the dot)
2520
							$|[^\p{L}\p{M}\p{N}\-]
2521
							| # or
2522
							# a dot followed by end of line or a non-domain character
2523
							\.(?=$|[^\p{L}\p{M}\p{N}\-])
2524
						)';
2525
2526
						$data = preg_replace('~' . $email_regex . '~xi' . ($context['utf8'] ? 'u' : ''), '[email]$0[/email]', $data);
2527
					}
2528
				}
2529
			}
2530
2531
			// Restore any placeholders
2532
			$data = strtr($data, $placeholders);
2533
2534
			$data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));
2535
2536
			// If it wasn't changed, no copying or other boring stuff has to happen!
2537
			if ($data != substr($message, $last_pos, $pos - $last_pos))
2538
			{
2539
				$message = substr($message, 0, $last_pos) . $data . substr($message, $pos);
2540
2541
				// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
2542
				$old_pos = strlen($data) + $last_pos;
2543
				$pos = strpos($message, '[', $last_pos);
2544
				$pos = $pos === false ? $old_pos : min($pos, $old_pos);
2545
			}
2546
		}
2547
2548
		// Are we there yet?  Are we there yet?
2549
		if ($pos >= strlen($message) - 1)
2550
			break;
2551
2552
		$tag_character = strtolower($message[$pos + 1]);
2553
2554
		if ($tag_character == '/' && !empty($open_tags))
2555
		{
2556
			$pos2 = strpos($message, ']', $pos + 1);
2557
			if ($pos2 == $pos + 2)
2558
				continue;
2559
2560
			$look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));
2561
2562
			// A closing tag that doesn't match any open tags? Skip it.
2563
			if (!in_array($look_for, array_map(function($code)
2564
			{
2565
				return $code['tag'];
2566
			}, $open_tags)))
2567
				continue;
2568
2569
			$to_close = array();
2570
			$block_level = null;
2571
2572
			do
2573
			{
2574
				$tag = array_pop($open_tags);
2575
				if (!$tag)
2576
					break;
2577
2578
				if (!empty($tag['block_level']))
2579
				{
2580
					// Only find out if we need to.
2581
					if ($block_level === false)
2582
					{
2583
						array_push($open_tags, $tag);
2584
						break;
2585
					}
2586
2587
					// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
2588
					if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]]))
2589
					{
2590
						foreach ($bbc_codes[$look_for[0]] as $temp)
2591
							if ($temp['tag'] == $look_for)
2592
							{
2593
								$block_level = !empty($temp['block_level']);
2594
								break;
2595
							}
2596
					}
2597
2598
					if ($block_level !== true)
2599
					{
2600
						$block_level = false;
2601
						array_push($open_tags, $tag);
2602
						break;
2603
					}
2604
				}
2605
2606
				$to_close[] = $tag;
2607
			}
2608
			while ($tag['tag'] != $look_for);
2609
2610
			// Did we just eat through everything and not find it?
2611
			if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
2612
			{
2613
				$open_tags = $to_close;
2614
				continue;
2615
			}
2616
			elseif (!empty($to_close) && $tag['tag'] != $look_for)
2617
			{
2618
				if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]]))
2619
				{
2620
					foreach ($bbc_codes[$look_for[0]] as $temp)
2621
						if ($temp['tag'] == $look_for)
2622
						{
2623
							$block_level = !empty($temp['block_level']);
2624
							break;
2625
						}
2626
				}
2627
2628
				// We're not looking for a block level tag (or maybe even a tag that exists...)
2629
				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...
2630
				{
2631
					foreach ($to_close as $tag)
2632
						array_push($open_tags, $tag);
2633
					continue;
2634
				}
2635
			}
2636
2637
			foreach ($to_close as $tag)
2638
			{
2639
				$message = substr($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1);
2640
				$pos += strlen($tag['after']) + 2;
2641
				$pos2 = $pos - 1;
2642
2643
				// See the comment at the end of the big loop - just eating whitespace ;).
2644
				$whitespace_regex = '';
2645
				if (!empty($tag['block_level']))
2646
					$whitespace_regex .= '(&nbsp;|\s)*(<br\s*/?' . '>)?';
2647
				// Trim one line of whitespace after unnested tags, but all of it after nested ones
2648
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
2649
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2650
2651
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
2652
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2653
			}
2654
2655
			if (!empty($to_close))
2656
			{
2657
				$to_close = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $to_close is dead and can be removed.
Loading history...
2658
				$pos--;
2659
			}
2660
2661
			continue;
2662
		}
2663
2664
		// No tags for this character, so just keep going (fastest possible course.)
2665
		if (!isset($bbc_codes[$tag_character]))
2666
			continue;
2667
2668
		$inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
2669
		$tag = null;
2670
		foreach ($bbc_codes[$tag_character] as $possible)
2671
		{
2672
			$pt_strlen = strlen($possible['tag']);
2673
2674
			// Not a match?
2675
			if (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag'])
2676
				continue;
2677
2678
			$next_c = isset($message[$pos + 1 + $pt_strlen]) ? $message[$pos + 1 + $pt_strlen] : '';
2679
2680
			// A tag is the last char maybe
2681
			if ($next_c == '')
2682
				break;
2683
2684
			// A test validation?
2685
			if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0)
2686
				continue;
2687
			// Do we want parameters?
2688
			elseif (!empty($possible['parameters']))
2689
			{
2690
				// Are all the parameters optional?
2691
				$param_required = false;
2692
				foreach ($possible['parameters'] as $param)
2693
				{
2694
					if (empty($param['optional']))
2695
					{
2696
						$param_required = true;
2697
						break;
2698
					}
2699
				}
2700
2701
				if ($param_required && $next_c != ' ')
2702
					continue;
2703
			}
2704
			elseif (isset($possible['type']))
2705
			{
2706
				// Do we need an equal sign?
2707
				if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=')
2708
					continue;
2709
				// Maybe we just want a /...
2710
				if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + $pt_strlen, 2) != '/]' && substr($message, $pos + 1 + $pt_strlen, 3) != ' /]')
2711
					continue;
2712
				// An immediate ]?
2713
				if ($possible['type'] == 'unparsed_content' && $next_c != ']')
2714
					continue;
2715
			}
2716
			// No type means 'parsed_content', which demands an immediate ] without parameters!
2717
			elseif ($next_c != ']')
2718
				continue;
2719
2720
			// Check allowed tree?
2721
			if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
2722
				continue;
2723
			elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
2724
				continue;
2725
			// If this is in the list of disallowed child tags, don't parse it.
2726
			elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
2727
				continue;
2728
2729
			$pos1 = $pos + 1 + $pt_strlen + 1;
2730
2731
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
2732
			if ($possible['tag'] == 'quote')
2733
			{
2734
				// Start with standard
2735
				$quote_alt = false;
2736
				foreach ($open_tags as $open_quote)
2737
				{
2738
					// Every parent quote this quote has flips the styling
2739
					if ($open_quote['tag'] == 'quote')
2740
						$quote_alt = !$quote_alt;
0 ignored issues
show
introduced by
The condition $quote_alt is always false.
Loading history...
2741
				}
2742
				// Add a class to the quote to style alternating blockquotes
2743
				$possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">'));
2744
			}
2745
2746
			// This is long, but it makes things much easier and cleaner.
2747
			if (!empty($possible['parameters']))
2748
			{
2749
				// Build a regular expression for each parameter for the current tag.
2750
				$regex_key = $smcFunc['json_encode']($possible['parameters']);
2751
				if (!isset($params_regexes[$regex_key]))
2752
				{
2753
					$params_regexes[$regex_key] = '';
2754
2755
					foreach ($possible['parameters'] as $p => $info)
2756
						$params_regexes[$regex_key] .= '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . '\s*)' . (empty($info['optional']) ? '' : '?');
2757
				}
2758
2759
				// Extract the string that potentially holds our parameters.
2760
				$blob = preg_split('~\[/?(?:' . $alltags_regex . ')~i', substr($message, $pos));
2761
				$blobs = preg_split('~\]~i', $blob[1]);
2762
2763
				$splitters = implode('=|', array_keys($possible['parameters'])) . '=';
2764
2765
				// Progressively append more blobs until we find our parameters or run out of blobs
2766
				$blob_counter = 1;
2767
				while ($blob_counter <= count($blobs))
0 ignored issues
show
Bug introduced by
It seems like $blobs can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

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

2767
				while ($blob_counter <= count(/** @scrutinizer ignore-type */ $blobs))
Loading history...
2768
				{
2769
					$given_param_string = implode(']', array_slice($blobs, 0, $blob_counter++));
0 ignored issues
show
Bug introduced by
It seems like $blobs can also be of type false; however, parameter $array of array_slice() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

2769
					$given_param_string = implode(']', array_slice(/** @scrutinizer ignore-type */ $blobs, 0, $blob_counter++));
Loading history...
2770
2771
					$given_params = preg_split('~\s(?=(' . $splitters . '))~i', $given_param_string);
2772
					sort($given_params, SORT_STRING);
0 ignored issues
show
Bug introduced by
It seems like $given_params can also be of type false; however, parameter $array of sort() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

2772
					sort(/** @scrutinizer ignore-type */ $given_params, SORT_STRING);
Loading history...
2773
2774
					$match = preg_match('~^' . $params_regexes[$regex_key] . '$~i', implode(' ', $given_params), $matches) !== 0;
2775
2776
					if ($match)
2777
						break;
2778
				}
2779
2780
				// Didn't match our parameter list, try the next possible.
2781
				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...
2782
					continue;
2783
2784
				$params = array();
2785
				for ($i = 1, $n = count($matches); $i < $n; $i += 2)
2786
				{
2787
					$key = strtok(ltrim($matches[$i]), '=');
2788
					if ($key === false)
2789
						continue;
2790
					elseif (isset($possible['parameters'][$key]['value']))
2791
						$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
2792
					elseif (isset($possible['parameters'][$key]['validate']))
2793
						$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
2794
					else
2795
						$params['{' . $key . '}'] = $matches[$i + 1];
2796
2797
					// Just to make sure: replace any $ or { so they can't interpolate wrongly.
2798
					$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
2799
				}
2800
2801
				foreach ($possible['parameters'] as $p => $info)
2802
				{
2803
					if (!isset($params['{' . $p . '}']))
2804
					{
2805
						if (!isset($info['default']))
2806
							$params['{' . $p . '}'] = '';
2807
						elseif (isset($possible['parameters'][$p]['value']))
2808
							$params['{' . $p . '}'] = strtr($possible['parameters'][$p]['value'], array('$1' => $info['default']));
2809
						elseif (isset($possible['parameters'][$p]['validate']))
2810
							$params['{' . $p . '}'] = $possible['parameters'][$p]['validate']($info['default']);
2811
						else
2812
							$params['{' . $p . '}'] = $info['default'];
2813
					}
2814
				}
2815
2816
				$tag = $possible;
2817
2818
				// Put the parameters into the string.
2819
				if (isset($tag['before']))
2820
					$tag['before'] = strtr($tag['before'], $params);
2821
				if (isset($tag['after']))
2822
					$tag['after'] = strtr($tag['after'], $params);
2823
				if (isset($tag['content']))
2824
					$tag['content'] = strtr($tag['content'], $params);
2825
2826
				$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...
2827
			}
2828
			else
2829
			{
2830
				$tag = $possible;
2831
				$params = array();
2832
			}
2833
			break;
2834
		}
2835
2836
		// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
2837
		if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
2838
		{
2839
			if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
2840
				continue;
2841
2842
			$tag = $itemcodes[$message[$pos + 1]];
2843
2844
			// First let's set up the tree: it needs to be in a list, or after an li.
2845
			if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
2846
			{
2847
				$open_tags[] = array(
2848
					'tag' => 'list',
2849
					'after' => '</ul>',
2850
					'block_level' => true,
2851
					'require_children' => array('li'),
2852
					'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2853
				);
2854
				$code = '<ul class="bbc_list">';
2855
			}
2856
			// We're in a list item already: another itemcode?  Close it first.
2857
			elseif ($inside['tag'] == 'li')
2858
			{
2859
				array_pop($open_tags);
2860
				$code = '</li>';
2861
			}
2862
			else
2863
				$code = '';
2864
2865
			// Now we open a new tag.
2866
			$open_tags[] = array(
2867
				'tag' => 'li',
2868
				'after' => '</li>',
2869
				'trim' => 'outside',
2870
				'block_level' => true,
2871
				'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2872
			);
2873
2874
			// First, open the tag...
2875
			$code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
2876
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
2877
			$pos += strlen($code) - 1 + 2;
2878
2879
			// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
2880
			$pos2 = strpos($message, '<br>', $pos);
2881
			$pos3 = strpos($message, '[/', $pos);
2882
			if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
2883
			{
2884
				preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 4), $matches);
2885
				$message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
2886
2887
				$open_tags[count($open_tags) - 2]['after'] = '</ul>';
2888
			}
2889
			// Tell the [list] that it needs to close specially.
2890
			else
2891
			{
2892
				// Move the li over, because we're not sure what we'll hit.
2893
				$open_tags[count($open_tags) - 1]['after'] = '';
2894
				$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
2895
			}
2896
2897
			continue;
2898
		}
2899
2900
		// Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
2901
		if ($tag === null && $inside !== null && !empty($inside['require_children']))
2902
		{
2903
			array_pop($open_tags);
2904
2905
			$message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
2906
			$pos += strlen($inside['after']) - 1 + 2;
2907
		}
2908
2909
		// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
2910
		if ($tag === null)
2911
			continue;
2912
2913
		// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
2914
		if (isset($inside['disallow_children']))
2915
			$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
2916
2917
		// Is this tag disabled?
2918
		if (isset($disabled[$tag['tag']]))
2919
		{
2920
			if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
2921
			{
2922
				$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
2923
				$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
2924
				$tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
2925
			}
2926
			elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
2927
			{
2928
				$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
2929
				$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
2930
			}
2931
			else
2932
				$tag['content'] = $tag['disabled_content'];
2933
		}
2934
2935
		// we use this a lot
2936
		$tag_strlen = strlen($tag['tag']);
2937
2938
		// The only special case is 'html', which doesn't need to close things.
2939
		if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
2940
		{
2941
			$n = count($open_tags) - 1;
2942
			while (empty($open_tags[$n]['block_level']) && $n >= 0)
2943
				$n--;
2944
2945
			// Close all the non block level tags so this tag isn't surrounded by them.
2946
			for ($i = count($open_tags) - 1; $i > $n; $i--)
2947
			{
2948
				$message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
2949
				$ot_strlen = strlen($open_tags[$i]['after']);
2950
				$pos += $ot_strlen + 2;
2951
				$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...
2952
2953
				// Trim or eat trailing stuff... see comment at the end of the big loop.
2954
				$whitespace_regex = '';
2955
				if (!empty($tag['block_level']))
2956
					$whitespace_regex .= '(&nbsp;|\s)*(<br>)?';
2957
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
2958
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2959
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
2960
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2961
2962
				array_pop($open_tags);
2963
			}
2964
		}
2965
2966
		// Can't read past the end of the message
2967
		$pos1 = min(strlen($message), $pos1);
2968
2969
		// No type means 'parsed_content'.
2970
		if (!isset($tag['type']))
2971
		{
2972
			$open_tags[] = $tag;
2973
			$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
2974
			$pos += strlen($tag['before']) - 1 + 2;
2975
		}
2976
		// Don't parse the content, just skip it.
2977
		elseif ($tag['type'] == 'unparsed_content')
2978
		{
2979
			$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
2980
			if ($pos2 === false)
2981
				continue;
2982
2983
			$data = substr($message, $pos1, $pos2 - $pos1);
2984
2985
			if (!empty($tag['block_level']) && substr($data, 0, 4) == '<br>')
2986
				$data = substr($data, 4);
2987
2988
			if (isset($tag['validate']))
2989
				$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...
2990
2991
			$code = strtr($tag['content'], array('$1' => $data));
2992
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
2993
2994
			$pos += strlen($code) - 1 + 2;
2995
			$last_pos = $pos + 1;
2996
		}
2997
		// Don't parse the content, just skip it.
2998
		elseif ($tag['type'] == 'unparsed_equals_content')
2999
		{
3000
			// The value may be quoted for some tags - check.
3001
			if (isset($tag['quoted']))
3002
			{
3003
				$quoted = substr($message, $pos1, 6) == '&quot;';
3004
				if ($tag['quoted'] != 'optional' && !$quoted)
3005
					continue;
3006
3007
				if ($quoted)
3008
					$pos1 += 6;
3009
			}
3010
			else
3011
				$quoted = false;
3012
3013
			$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...
3014
			if ($pos2 === false)
3015
				continue;
3016
3017
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3018
			if ($pos3 === false)
3019
				continue;
3020
3021
			$data = array(
3022
				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...
3023
				substr($message, $pos1, $pos2 - $pos1)
3024
			);
3025
3026
			if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '<br>')
3027
				$data[0] = substr($data[0], 4);
3028
3029
			// Validation for my parking, please!
3030
			if (isset($tag['validate']))
3031
				$tag['validate']($tag, $data, $disabled, $params);
3032
3033
			$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
3034
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3035
			$pos += strlen($code) - 1 + 2;
3036
		}
3037
		// A closed tag, with no content or value.
3038
		elseif ($tag['type'] == 'closed')
3039
		{
3040
			$pos2 = strpos($message, ']', $pos);
3041
			$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
3042
			$pos += strlen($tag['content']) - 1 + 2;
3043
		}
3044
		// This one is sorta ugly... :/.  Unfortunately, it's needed for flash.
3045
		elseif ($tag['type'] == 'unparsed_commas_content')
3046
		{
3047
			$pos2 = strpos($message, ']', $pos1);
3048
			if ($pos2 === false)
3049
				continue;
3050
3051
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3052
			if ($pos3 === false)
3053
				continue;
3054
3055
			// We want $1 to be the content, and the rest to be csv.
3056
			$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
3057
			$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
3058
3059
			if (isset($tag['validate']))
3060
				$tag['validate']($tag, $data, $disabled, $params);
3061
3062
			$code = $tag['content'];
3063
			foreach ($data as $k => $d)
3064
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3065
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3066
			$pos += strlen($code) - 1 + 2;
3067
		}
3068
		// This has parsed content, and a csv value which is unparsed.
3069
		elseif ($tag['type'] == 'unparsed_commas')
3070
		{
3071
			$pos2 = strpos($message, ']', $pos1);
3072
			if ($pos2 === false)
3073
				continue;
3074
3075
			$data = explode(',', substr($message, $pos1, $pos2 - $pos1));
3076
3077
			if (isset($tag['validate']))
3078
				$tag['validate']($tag, $data, $disabled, $params);
3079
3080
			// Fix after, for disabled code mainly.
3081
			foreach ($data as $k => $d)
3082
				$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
3083
3084
			$open_tags[] = $tag;
3085
3086
			// Replace them out, $1, $2, $3, $4, etc.
3087
			$code = $tag['before'];
3088
			foreach ($data as $k => $d)
3089
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3090
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1);
3091
			$pos += strlen($code) - 1 + 2;
3092
		}
3093
		// A tag set to a value, parsed or not.
3094
		elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
3095
		{
3096
			// The value may be quoted for some tags - check.
3097
			if (isset($tag['quoted']))
3098
			{
3099
				$quoted = substr($message, $pos1, 6) == '&quot;';
3100
				if ($tag['quoted'] != 'optional' && !$quoted)
3101
					continue;
3102
3103
				if ($quoted)
3104
					$pos1 += 6;
3105
			}
3106
			else
3107
				$quoted = false;
3108
3109
			$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...
3110
			if ($pos2 === false)
3111
				continue;
3112
3113
			$data = substr($message, $pos1, $pos2 - $pos1);
3114
3115
			// Validation for my parking, please!
3116
			if (isset($tag['validate']))
3117
				$tag['validate']($tag, $data, $disabled, $params);
3118
3119
			// For parsed content, we must recurse to avoid security problems.
3120
			if ($tag['type'] != 'unparsed_equals')
3121
				$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
3122
3123
			$tag['after'] = strtr($tag['after'], array('$1' => $data));
3124
3125
			$open_tags[] = $tag;
3126
3127
			$code = strtr($tag['before'], array('$1' => $data));
3128
			$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...
3129
			$pos += strlen($code) - 1 + 2;
3130
		}
3131
3132
		// If this is block level, eat any breaks after it.
3133
		if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '<br>')
3134
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 5);
3135
3136
		// Are we trimming outside this tag?
3137
		if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
3138
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
3139
	}
3140
3141
	// Close any remaining tags.
3142
	while ($tag = array_pop($open_tags))
3143
		$message .= "\n" . $tag['after'] . "\n";
3144
3145
	// Parse the smileys within the parts where it can be done safely.
3146
	if ($smileys === true)
3147
	{
3148
		$message_parts = explode("\n", $message);
3149
		for ($i = 0, $n = count($message_parts); $i < $n; $i += 2)
3150
			parsesmileys($message_parts[$i]);
3151
3152
		$message = implode('', $message_parts);
3153
	}
3154
3155
	// No smileys, just get rid of the markers.
3156
	else
3157
		$message = strtr($message, array("\n" => ''));
3158
3159
	if ($message !== '' && $message[0] === ' ')
3160
		$message = '&nbsp;' . substr($message, 1);
3161
3162
	// Cleanup whitespace.
3163
	$message = strtr($message, array('  ' => ' &nbsp;', "\r" => '', "\n" => '<br>', '<br> ' => '<br>&nbsp;', '&#13;' => "\n"));
3164
3165
	// Allow mods access to what parse_bbc created
3166
	call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
3167
3168
	// Cache the output if it took some time...
3169
	if (isset($cache_key, $cache_t) && microtime(true) - $cache_t > 0.05)
3170
		cache_put_data($cache_key, $message, 240);
3171
3172
	// If this was a force parse revert if needed.
3173
	if (!empty($parse_tags))
3174
	{
3175
		$alltags_regex = empty($real_alltags_regex) ? '' : $real_alltags_regex;
3176
		unset($real_alltags_regex);
3177
	}
3178
	elseif (!empty($bbc_codes))
3179
		$bbc_lang_locales[$txt['lang_locale']] = $bbc_codes;
3180
3181
	return $message;
3182
}
3183
3184
/**
3185
 * Parse smileys in the passed message.
3186
 *
3187
 * The smiley parsing function which makes pretty faces appear :).
3188
 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
3189
 * These are specifically not parsed in code tags [url=mailto:[email protected]]
3190
 * Caches the smileys from the database or array in memory.
3191
 * Doesn't return anything, but rather modifies message directly.
3192
 *
3193
 * @param string &$message The message to parse smileys in
3194
 */
3195
function parsesmileys(&$message)
3196
{
3197
	global $modSettings, $txt, $user_info, $context, $smcFunc;
3198
	static $smileyPregSearch = null, $smileyPregReplacements = array();
3199
3200
	// No smiley set at all?!
3201
	if ($user_info['smiley_set'] == 'none' || trim($message) == '')
3202
		return;
3203
3204
	// Maybe a mod wants to implement an alternative method (e.g. emojis instead of images)
3205
	call_integration_hook('integrate_smileys', array(&$smileyPregSearch, &$smileyPregReplacements));
3206
3207
	// If smileyPregSearch hasn't been set, do it now.
3208
	if (empty($smileyPregSearch))
3209
	{
3210
		// Cache for longer when customized smiley codes aren't enabled
3211
		$cache_time = empty($modSettings['smiley_enable']) ? 7200 : 480;
3212
3213
		// Load the smileys in reverse order by length so they don't get parsed incorrectly.
3214
		if (($temp = cache_get_data('parsing_smileys_' . $user_info['smiley_set'], $cache_time)) == null)
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $temp = cache_get_data('...ley_set'], $cache_time) of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
3215
		{
3216
			$result = $smcFunc['db_query']('', '
3217
				SELECT s.code, f.filename, s.description
3218
				FROM {db_prefix}smileys AS s
3219
					JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
3220
				WHERE f.smiley_set = {string:smiley_set}' . (empty($modSettings['smiley_enable']) ? '
3221
					AND s.code IN ({array_string:default_codes})' : '') . '
3222
				ORDER BY LENGTH(s.code) DESC',
3223
				array(
3224
					'default_codes' => array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'),
3225
					'smiley_set' => $user_info['smiley_set'],
3226
				)
3227
			);
3228
			$smileysfrom = array();
3229
			$smileysto = array();
3230
			$smileysdescs = array();
3231
			while ($row = $smcFunc['db_fetch_assoc']($result))
3232
			{
3233
				$smileysfrom[] = $row['code'];
3234
				$smileysto[] = $smcFunc['htmlspecialchars']($row['filename']);
3235
				$smileysdescs[] = !empty($txt['icon_' . strtolower($row['description'])]) ? $txt['icon_' . strtolower($row['description'])] : $row['description'];
3236
			}
3237
			$smcFunc['db_free_result']($result);
3238
3239
			cache_put_data('parsing_smileys_' . $user_info['smiley_set'], array($smileysfrom, $smileysto, $smileysdescs), $cache_time);
3240
		}
3241
		else
3242
			list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
3243
3244
		// The non-breaking-space is a complex thing...
3245
		$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
3246
3247
		// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:[email protected]] doesn't parse the :D smiley)
3248
		$smileyPregReplacements = array();
3249
		$searchParts = array();
3250
		$smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
3251
3252
		for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
3253
		{
3254
			$specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES);
3255
			$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">';
3256
3257
			$smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
3258
3259
			$searchParts[] = $smileysfrom[$i];
3260
			if ($smileysfrom[$i] != $specialChars)
3261
			{
3262
				$smileyPregReplacements[$specialChars] = $smileyCode;
3263
				$searchParts[] = $specialChars;
3264
3265
				// Some 2.0 hex htmlchars are in there as 3 digits; allow for finding leading 0 or not
3266
				$specialChars2 = preg_replace('/&#(\d{2});/', '&#0$1;', $specialChars);
3267
				if ($specialChars2 != $specialChars)
3268
				{
3269
					$smileyPregReplacements[$specialChars2] = $smileyCode;
3270
					$searchParts[] = $specialChars2;
3271
				}
3272
			}
3273
		}
3274
3275
		$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

3275
		$smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?<![a-zA-Z0-9])\(|^)(' . /** @scrutinizer ignore-type */ build_regex($searchParts, '~') . ')(?=[^[:alpha:]0-9]|$)~' . ($context['utf8'] ? 'u' : '');
Loading history...
3276
	}
3277
3278
	// Replace away!
3279
	$message = preg_replace_callback($smileyPregSearch, function($matches) use ($smileyPregReplacements)
3280
		{
3281
			return $smileyPregReplacements[$matches[1]];
3282
		}, $message);
3283
}
3284
3285
/**
3286
 * Highlight any code.
3287
 *
3288
 * Uses PHP's highlight_string() to highlight PHP syntax
3289
 * does special handling to keep the tabs in the code available.
3290
 * used to parse PHP code from inside [code] and [php] tags.
3291
 *
3292
 * @param string $code The code
3293
 * @return string The code with highlighted HTML.
3294
 */
3295
function highlight_php_code($code)
3296
{
3297
	// Remove special characters.
3298
	$code = un_htmlspecialchars(strtr($code, array('<br />' => "\n", '<br>' => "\n", "\t" => 'SMF_TAB();', '&#91;' => '[')));
3299
3300
	$oldlevel = error_reporting(0);
3301
3302
	$buffer = str_replace(array("\n", "\r"), '', @highlight_string($code, true));
3303
3304
	error_reporting($oldlevel);
3305
3306
	// Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
3307
	$buffer = preg_replace('~SMF_TAB(?:</(?:font|span)><(?:font color|span style)="[^"]*?">)?\\(\\);~', '<pre style="display: inline;">' . "\t" . '</pre>', $buffer);
3308
3309
	return strtr($buffer, array('\'' => '&#039;', '<code>' => '', '</code>' => ''));
3310
}
3311
3312
/**
3313
 * Gets the appropriate URL to use for images (or whatever) when using SSL
3314
 *
3315
 * The returned URL may or may not be a proxied URL, depending on the situation.
3316
 * Mods can implement alternative proxies using the 'integrate_proxy' hook.
3317
 *
3318
 * @param string $url The original URL of the requested resource
3319
 * @return string The URL to use
3320
 */
3321
function get_proxied_url($url)
3322
{
3323
	global $boardurl, $image_proxy_enabled, $image_proxy_secret, $user_info;
3324
3325
	// Only use the proxy if enabled, and never for robots
3326
	if (empty($image_proxy_enabled) || !empty($user_info['possibly_robot']))
3327
		return $url;
3328
3329
	$parsedurl = parse_url($url);
3330
3331
	// Don't bother with HTTPS URLs, schemeless URLs, or obviously invalid URLs
3332
	if (empty($parsedurl['scheme']) || empty($parsedurl['host']) || empty($parsedurl['path']) || $parsedurl['scheme'] === 'https')
3333
		return $url;
3334
3335
	// We don't need to proxy our own resources
3336
	if ($parsedurl['host'] === parse_url($boardurl, PHP_URL_HOST))
3337
		return strtr($url, array('http://' => 'https://'));
3338
3339
	// By default, use SMF's own image proxy script
3340
	$proxied_url = strtr($boardurl, array('http://' => 'https://')) . '/proxy.php?request=' . urlencode($url) . '&hash=' . hash_hmac('sha1', $url, $image_proxy_secret);
3341
3342
	// Allow mods to easily implement an alternative proxy
3343
	// MOD AUTHORS: To add settings UI for your proxy, use the integrate_general_settings hook.
3344
	call_integration_hook('integrate_proxy', array($url, &$proxied_url));
3345
3346
	return $proxied_url;
3347
}
3348
3349
/**
3350
 * Make sure the browser doesn't come back and repost the form data.
3351
 * Should be used whenever anything is posted.
3352
 *
3353
 * @param string $setLocation The URL to redirect them to
3354
 * @param bool $refresh Whether to use a meta refresh instead
3355
 * @param bool $permanent Whether to send a 301 Moved Permanently instead of a 302 Moved Temporarily
3356
 */
3357
function redirectexit($setLocation = '', $refresh = false, $permanent = false)
3358
{
3359
	global $scripturl, $context, $modSettings, $db_show_debug, $db_cache;
3360
3361
	// In case we have mail to send, better do that - as obExit doesn't always quite make it...
3362
	if (!empty($context['flush_mail']))
3363
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
3364
		AddMailQueue(true);
3365
3366
	$add = preg_match('~^(ftp|http)[s]?://~', $setLocation) == 0 && substr($setLocation, 0, 6) != 'about:';
3367
3368
	if ($add)
3369
		$setLocation = $scripturl . ($setLocation != '' ? '?' . $setLocation : '');
3370
3371
	// Put the session ID in.
3372
	if (defined('SID') && SID != '')
3373
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', $scripturl . '?' . SID . ';', $setLocation);
3374
	// Keep that debug in their for template debugging!
3375
	elseif (isset($_GET['debug']))
3376
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);
3377
3378
	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'])))
3379
	{
3380
		if (defined('SID') && SID != '')
3381
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '~') . '\?(?:' . SID . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~',
3382
				function($m) use ($scripturl)
3383
				{
3384
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . SID . (isset($m[2]) ? "$m[2]" : "");
3385
				}, $setLocation);
3386
		else
3387
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '~') . '\?((?:board|topic)=[^#"]+?)(#[^"]*?)?$~',
3388
				function($m) use ($scripturl)
3389
				{
3390
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html' . (isset($m[2]) ? "$m[2]" : "");
3391
				}, $setLocation);
3392
	}
3393
3394
	// Maybe integrations want to change where we are heading?
3395
	call_integration_hook('integrate_redirect', array(&$setLocation, &$refresh, &$permanent));
3396
3397
	// Set the header.
3398
	header('location: ' . str_replace(' ', '%20', $setLocation), true, $permanent ? 301 : 302);
3399
3400
	// Debugging.
3401
	if (isset($db_show_debug) && $db_show_debug === true)
3402
		$_SESSION['debug_redirect'] = $db_cache;
3403
3404
	obExit(false);
3405
}
3406
3407
/**
3408
 * Ends execution.  Takes care of template loading and remembering the previous URL.
3409
 *
3410
 * @param bool $header Whether to do the header
3411
 * @param bool $do_footer Whether to do the footer
3412
 * @param bool $from_index Whether we're coming from the board index
3413
 * @param bool $from_fatal_error Whether we're coming from a fatal error
3414
 */
3415
function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false)
3416
{
3417
	global $context, $settings, $modSettings, $txt, $smcFunc, $should_log;
3418
	static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
3419
3420
	// Attempt to prevent a recursive loop.
3421
	++$level;
3422
	if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
3423
		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...
3424
	if ($from_fatal_error)
3425
		$has_fatal_error = true;
3426
3427
	// Clear out the stat cache.
3428
	if (function_exists('trackStats'))
3429
		trackStats();
3430
3431
	// If we have mail to send, send it.
3432
	if (function_exists('AddMailQueue') && !empty($context['flush_mail']))
3433
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
3434
		AddMailQueue(true);
3435
3436
	$do_header = $header === null ? !$header_done : $header;
3437
	if ($do_footer === null)
3438
		$do_footer = $do_header;
3439
3440
	// Has the template/header been done yet?
3441
	if ($do_header)
3442
	{
3443
		// Was the page title set last minute? Also update the HTML safe one.
3444
		if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
3445
			$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3446
3447
		// Start up the session URL fixer.
3448
		ob_start('ob_sessrewrite');
3449
3450
		if (!empty($settings['output_buffers']) && is_string($settings['output_buffers']))
3451
			$buffers = explode(',', $settings['output_buffers']);
3452
		elseif (!empty($settings['output_buffers']))
3453
			$buffers = $settings['output_buffers'];
3454
		else
3455
			$buffers = array();
3456
3457
		if (isset($modSettings['integrate_buffer']))
3458
			$buffers = array_merge(explode(',', $modSettings['integrate_buffer']), $buffers);
3459
3460
		if (!empty($buffers))
3461
			foreach ($buffers as $function)
3462
			{
3463
				$call = call_helper($function, true);
3464
3465
				// Is it valid?
3466
				if (!empty($call))
3467
					ob_start($call);
0 ignored issues
show
Bug introduced by
It seems like $call can also be of type boolean; however, parameter $output_callback of ob_start() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

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

3467
					ob_start(/** @scrutinizer ignore-type */ $call);
Loading history...
3468
			}
3469
3470
		// Display the screen in the logical order.
3471
		template_header();
3472
		$header_done = true;
3473
	}
3474
	if ($do_footer)
3475
	{
3476
		loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main');
3477
3478
		// Anything special to put out?
3479
		if (!empty($context['insert_after_template']) && !isset($_REQUEST['xml']))
3480
			echo $context['insert_after_template'];
3481
3482
		// Just so we don't get caught in an endless loop of errors from the footer...
3483
		if (!$footer_done)
3484
		{
3485
			$footer_done = true;
3486
			template_footer();
3487
3488
			// (since this is just debugging... it's okay that it's after </html>.)
3489
			if (!isset($_REQUEST['xml']))
3490
				displayDebug();
3491
		}
3492
	}
3493
3494
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
3495
	if ($should_log)
3496
		$_SESSION['old_url'] = $_SERVER['REQUEST_URL'];
3497
3498
	// For session check verification.... don't switch browsers...
3499
	$_SESSION['USER_AGENT'] = empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT'];
3500
3501
	// Hand off the output to the portal, etc. we're integrated with.
3502
	call_integration_hook('integrate_exit', array($do_footer));
3503
3504
	// Don't exit if we're coming from index.php; that will pass through normally.
3505
	if (!$from_index)
3506
		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...
3507
}
3508
3509
/**
3510
 * Get the size of a specified image with better error handling.
3511
 *
3512
 * @todo see if it's better in Subs-Graphics, but one step at the time.
3513
 * Uses getimagesize() to determine the size of a file.
3514
 * Attempts to connect to the server first so it won't time out.
3515
 *
3516
 * @param string $url The URL of the image
3517
 * @return array|false The image size as array (width, height), or false on failure
3518
 */
3519
function url_image_size($url)
3520
{
3521
	global $sourcedir;
3522
3523
	// Make sure it is a proper URL.
3524
	$url = str_replace(' ', '%20', $url);
3525
3526
	// Can we pull this from the cache... please please?
3527
	if (($temp = cache_get_data('url_image_size-' . md5($url), 240)) !== null)
3528
		return $temp;
3529
	$t = microtime(true);
3530
3531
	// Get the host to pester...
3532
	preg_match('~^\w+://(.+?)/(.*)$~', $url, $match);
3533
3534
	// Can't figure it out, just try the image size.
3535
	if ($url == '' || $url == 'http://' || $url == 'https://')
3536
	{
3537
		return false;
3538
	}
3539
	elseif (!isset($match[1]))
3540
	{
3541
		$size = @getimagesize($url);
3542
	}
3543
	else
3544
	{
3545
		// Try to connect to the server... give it half a second.
3546
		$temp = 0;
3547
		$fp = @fsockopen($match[1], 80, $temp, $temp, 0.5);
3548
3549
		// Successful?  Continue...
3550
		if ($fp != false)
3551
		{
3552
			// Send the HEAD request (since we don't have to worry about chunked, HTTP/1.1 is fine here.)
3553
			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");
3554
3555
			// Read in the HTTP/1.1 or whatever.
3556
			$test = substr(fgets($fp, 11), -1);
3557
			fclose($fp);
3558
3559
			// See if it returned a 404/403 or something.
3560
			if ($test < 4)
3561
			{
3562
				$size = @getimagesize($url);
3563
3564
				// This probably means allow_url_fopen is off, let's try GD.
3565
				if ($size === false && function_exists('imagecreatefromstring'))
3566
				{
3567
					// It's going to hate us for doing this, but another request...
3568
					$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

3568
					$image = @imagecreatefromstring(/** @scrutinizer ignore-type */ fetch_web_data($url));
Loading history...
3569
					if ($image !== false)
3570
					{
3571
						$size = array(imagesx($image), imagesy($image));
3572
						imagedestroy($image);
3573
					}
3574
				}
3575
			}
3576
		}
3577
	}
3578
3579
	// If we didn't get it, we failed.
3580
	if (!isset($size))
3581
		$size = false;
3582
3583
	// If this took a long time, we may never have to do it again, but then again we might...
3584
	if (microtime(true) - $t > 0.8)
3585
		cache_put_data('url_image_size-' . md5($url), $size, 240);
3586
3587
	// Didn't work.
3588
	return $size;
3589
}
3590
3591
/**
3592
 * Sets up the basic theme context stuff.
3593
 *
3594
 * @param bool $forceload Whether to load the theme even if it's already loaded
3595
 */
3596
function setupThemeContext($forceload = false)
3597
{
3598
	global $modSettings, $user_info, $scripturl, $context, $settings, $options, $txt, $maintenance;
3599
	global $smcFunc;
3600
	static $loaded = false;
3601
3602
	// Under SSI this function can be called more then once.  That can cause some problems.
3603
	//   So only run the function once unless we are forced to run it again.
3604
	if ($loaded && !$forceload)
3605
		return;
3606
3607
	$loaded = true;
3608
3609
	$context['in_maintenance'] = !empty($maintenance);
3610
	$context['current_time'] = timeformat(time(), false);
3611
	$context['current_action'] = isset($_GET['action']) ? $smcFunc['htmlspecialchars']($_GET['action']) : '';
3612
	$context['random_news_line'] = array();
3613
3614
	// Get some news...
3615
	$context['news_lines'] = array_filter(explode("\n", str_replace("\r", '', trim(addslashes($modSettings['news'])))));
3616
	for ($i = 0, $n = count($context['news_lines']); $i < $n; $i++)
3617
	{
3618
		if (trim($context['news_lines'][$i]) == '')
3619
			continue;
3620
3621
		// Clean it up for presentation ;).
3622
		$context['news_lines'][$i] = parse_bbc(stripslashes(trim($context['news_lines'][$i])), true, 'news' . $i);
3623
	}
3624
3625
	if (!empty($context['news_lines']) && (!empty($modSettings['allow_guestAccess']) || $context['user']['is_logged']))
3626
		$context['random_news_line'] = $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)];
3627
3628
	if (!$user_info['is_guest'])
3629
	{
3630
		$context['user']['messages'] = &$user_info['messages'];
3631
		$context['user']['unread_messages'] = &$user_info['unread_messages'];
3632
		$context['user']['alerts'] = &$user_info['alerts'];
3633
3634
		// Personal message popup...
3635
		if ($user_info['unread_messages'] > (isset($_SESSION['unread_messages']) ? $_SESSION['unread_messages'] : 0))
3636
			$context['user']['popup_messages'] = true;
3637
		else
3638
			$context['user']['popup_messages'] = false;
3639
		$_SESSION['unread_messages'] = $user_info['unread_messages'];
3640
3641
		if (allowedTo('moderate_forum'))
3642
			$context['unapproved_members'] = !empty($modSettings['unapprovedMembers']) ? $modSettings['unapprovedMembers'] : 0;
3643
3644
		$context['user']['avatar'] = array();
3645
3646
		// Check for gravatar first since we might be forcing them...
3647
		if (($modSettings['gravatarEnabled'] && substr($user_info['avatar']['url'], 0, 11) == 'gravatar://') || !empty($modSettings['gravatarOverride']))
3648
		{
3649
			if (!empty($modSettings['gravatarAllowExtraEmail']) && stristr($user_info['avatar']['url'], 'gravatar://') && strlen($user_info['avatar']['url']) > 11)
3650
				$context['user']['avatar']['href'] = get_gravatar_url($smcFunc['substr']($user_info['avatar']['url'], 11));
3651
			else
3652
				$context['user']['avatar']['href'] = get_gravatar_url($user_info['email']);
3653
		}
3654
		// Uploaded?
3655
		elseif ($user_info['avatar']['url'] == '' && !empty($user_info['avatar']['id_attach']))
3656
			$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';
3657
		// Full URL?
3658
		elseif (strpos($user_info['avatar']['url'], 'http://') === 0 || strpos($user_info['avatar']['url'], 'https://') === 0)
3659
			$context['user']['avatar']['href'] = $user_info['avatar']['url'];
3660
		// Otherwise we assume it's server stored.
3661
		elseif ($user_info['avatar']['url'] != '')
3662
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/' . $smcFunc['htmlspecialchars']($user_info['avatar']['url']);
3663
		// No avatar at all? Fine, we have a big fat default avatar ;)
3664
		else
3665
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/default.png';
3666
3667
		if (!empty($context['user']['avatar']))
3668
			$context['user']['avatar']['image'] = '<img src="' . $context['user']['avatar']['href'] . '" alt="" class="avatar">';
3669
3670
		// Figure out how long they've been logged in.
3671
		$context['user']['total_time_logged_in'] = array(
3672
			'days' => floor($user_info['total_time_logged_in'] / 86400),
3673
			'hours' => floor(($user_info['total_time_logged_in'] % 86400) / 3600),
3674
			'minutes' => floor(($user_info['total_time_logged_in'] % 3600) / 60)
3675
		);
3676
	}
3677
	else
3678
	{
3679
		$context['user']['messages'] = 0;
3680
		$context['user']['unread_messages'] = 0;
3681
		$context['user']['avatar'] = array();
3682
		$context['user']['total_time_logged_in'] = array('days' => 0, 'hours' => 0, 'minutes' => 0);
3683
		$context['user']['popup_messages'] = false;
3684
3685
		if (!empty($modSettings['registration_method']) && $modSettings['registration_method'] == 1)
3686
			$txt['welcome_guest'] .= $txt['welcome_guest_activate'];
3687
3688
		// If we've upgraded recently, go easy on the passwords.
3689
		if (!empty($modSettings['disableHashTime']) && ($modSettings['disableHashTime'] == 1 || time() < $modSettings['disableHashTime']))
3690
			$context['disable_login_hashing'] = true;
3691
	}
3692
3693
	// Setup the main menu items.
3694
	setupMenuContext();
3695
3696
	// This is here because old index templates might still use it.
3697
	$context['show_news'] = !empty($settings['enable_news']);
3698
3699
	// This is done to allow theme authors to customize it as they want.
3700
	$context['show_pm_popup'] = $context['user']['popup_messages'] && !empty($options['popup_messages']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != 'pm');
3701
3702
	// 2.1+: Add the PM popup here instead. Theme authors can still override it simply by editing/removing the 'fPmPopup' in the array.
3703
	if ($context['show_pm_popup'])
3704
		addInlineJavaScript('
3705
		jQuery(document).ready(function($) {
3706
			new smc_Popup({
3707
				heading: ' . JavaScriptEscape($txt['show_personal_messages_heading']) . ',
3708
				content: ' . JavaScriptEscape(sprintf($txt['show_personal_messages'], $context['user']['unread_messages'], $scripturl . '?action=pm')) . ',
3709
				icon_class: \'main_icons mail_new\'
3710
			});
3711
		});');
3712
3713
	// Add a generic "Are you sure?" confirmation message.
3714
	addInlineJavaScript('
3715
	var smf_you_sure =' . JavaScriptEscape($txt['quickmod_confirm']) . ';');
3716
3717
	// Now add the capping code for avatars.
3718
	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')
3719
		addInlineCss('
3720
	img.avatar { max-width: ' . $modSettings['avatar_max_width_external'] . 'px; max-height: ' . $modSettings['avatar_max_height_external'] . 'px; }');
3721
3722
	// Add max image limits
3723
	if (!empty($modSettings['max_image_width']))
3724
		addInlineCss('
3725
	.postarea .bbc_img { max-width: ' . $modSettings['max_image_width'] . 'px; }');
3726
3727
	if (!empty($modSettings['max_image_height']))
3728
		addInlineCss('
3729
	.postarea .bbc_img { max-height: ' . $modSettings['max_image_height'] . 'px; }');
3730
3731
	// This looks weird, but it's because BoardIndex.php references the variable.
3732
	$context['common_stats']['latest_member'] = array(
3733
		'id' => $modSettings['latestMember'],
3734
		'name' => $modSettings['latestRealName'],
3735
		'href' => $scripturl . '?action=profile;u=' . $modSettings['latestMember'],
3736
		'link' => '<a href="' . $scripturl . '?action=profile;u=' . $modSettings['latestMember'] . '">' . $modSettings['latestRealName'] . '</a>',
3737
	);
3738
	$context['common_stats'] = array(
3739
		'total_posts' => comma_format($modSettings['totalMessages']),
3740
		'total_topics' => comma_format($modSettings['totalTopics']),
3741
		'total_members' => comma_format($modSettings['totalMembers']),
3742
		'latest_member' => $context['common_stats']['latest_member'],
3743
	);
3744
	$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']);
3745
3746
	if (empty($settings['theme_version']))
3747
		addJavaScriptVar('smf_scripturl', $scripturl);
3748
3749
	if (!isset($context['page_title']))
3750
		$context['page_title'] = '';
3751
3752
	// Set some specific vars.
3753
	$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3754
	$context['meta_keywords'] = !empty($modSettings['meta_keywords']) ? $smcFunc['htmlspecialchars']($modSettings['meta_keywords']) : '';
3755
3756
	// Content related meta tags, including Open Graph
3757
	$context['meta_tags'][] = array('property' => 'og:site_name', 'content' => $context['forum_name']);
3758
	$context['meta_tags'][] = array('property' => 'og:title', 'content' => $context['page_title_html_safe']);
3759
3760
	if (!empty($context['meta_keywords']))
3761
		$context['meta_tags'][] = array('name' => 'keywords', 'content' => $context['meta_keywords']);
3762
3763
	if (!empty($context['canonical_url']))
3764
		$context['meta_tags'][] = array('property' => 'og:url', 'content' => $context['canonical_url']);
3765
3766
	if (!empty($settings['og_image']))
3767
		$context['meta_tags'][] = array('property' => 'og:image', 'content' => $settings['og_image']);
3768
3769
	if (!empty($context['meta_description']))
3770
	{
3771
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['meta_description']);
3772
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['meta_description']);
3773
	}
3774
	else
3775
	{
3776
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['page_title_html_safe']);
3777
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['page_title_html_safe']);
3778
	}
3779
3780
	call_integration_hook('integrate_theme_context');
3781
}
3782
3783
/**
3784
 * Helper function to set the system memory to a needed value
3785
 * - If the needed memory is greater than current, will attempt to get more
3786
 * - if in_use is set to true, will also try to take the current memory usage in to account
3787
 *
3788
 * @param string $needed The amount of memory to request, if needed, like 256M
3789
 * @param bool $in_use Set to true to account for current memory usage of the script
3790
 * @return boolean True if we have at least the needed memory
3791
 */
3792
function setMemoryLimit($needed, $in_use = false)
3793
{
3794
	// everything in bytes
3795
	$memory_current = memoryReturnBytes(ini_get('memory_limit'));
3796
	$memory_needed = memoryReturnBytes($needed);
3797
3798
	// should we account for how much is currently being used?
3799
	if ($in_use)
3800
		$memory_needed += function_exists('memory_get_usage') ? memory_get_usage() : (2 * 1048576);
3801
3802
	// if more is needed, request it
3803
	if ($memory_current < $memory_needed)
3804
	{
3805
		@ini_set('memory_limit', ceil($memory_needed / 1048576) . 'M');
3806
		$memory_current = memoryReturnBytes(ini_get('memory_limit'));
3807
	}
3808
3809
	$memory_current = max($memory_current, memoryReturnBytes(get_cfg_var('memory_limit')));
3810
3811
	// return success or not
3812
	return (bool) ($memory_current >= $memory_needed);
3813
}
3814
3815
/**
3816
 * Helper function to convert memory string settings to bytes
3817
 *
3818
 * @param string $val The byte string, like 256M or 1G
3819
 * @return integer The string converted to a proper integer in bytes
3820
 */
3821
function memoryReturnBytes($val)
3822
{
3823
	if (is_integer($val))
0 ignored issues
show
introduced by
The condition is_integer($val) is always false.
Loading history...
3824
		return $val;
3825
3826
	// Separate the number from the designator
3827
	$val = trim($val);
3828
	$num = intval(substr($val, 0, strlen($val) - 1));
3829
	$last = strtolower(substr($val, -1));
3830
3831
	// convert to bytes
3832
	switch ($last)
3833
	{
3834
		case 'g':
3835
			$num *= 1024;
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
3836
		case 'm':
3837
			$num *= 1024;
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
3838
		case 'k':
3839
			$num *= 1024;
3840
	}
3841
	return $num;
3842
}
3843
3844
/**
3845
 * The header template
3846
 */
3847
function template_header()
3848
{
3849
	global $txt, $modSettings, $context, $user_info, $boarddir, $cachedir, $cache_enable;
3850
3851
	setupThemeContext();
3852
3853
	// Print stuff to prevent caching of pages (except on attachment errors, etc.)
3854
	if (empty($context['no_last_modified']))
3855
	{
3856
		header('expires: Mon, 26 Jul 1997 05:00:00 GMT');
3857
		header('last-modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
3858
3859
		// Are we debugging the template/html content?
3860
		if (!isset($_REQUEST['xml']) && isset($_GET['debug']) && !isBrowser('ie'))
3861
			header('content-type: application/xhtml+xml');
3862
		elseif (!isset($_REQUEST['xml']))
3863
			header('content-type: text/html; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
3864
	}
3865
3866
	header('content-type: text/' . (isset($_REQUEST['xml']) ? 'xml' : 'html') . '; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
3867
3868
	// We need to splice this in after the body layer, or after the main layer for older stuff.
3869
	if ($context['in_maintenance'] && $context['user']['is_admin'])
3870
	{
3871
		$position = array_search('body', $context['template_layers']);
3872
		if ($position === false)
3873
			$position = array_search('main', $context['template_layers']);
3874
3875
		if ($position !== false)
3876
		{
3877
			$before = array_slice($context['template_layers'], 0, $position + 1);
3878
			$after = array_slice($context['template_layers'], $position + 1);
3879
			$context['template_layers'] = array_merge($before, array('maint_warning'), $after);
3880
		}
3881
	}
3882
3883
	$checked_securityFiles = false;
3884
	$showed_banned = false;
3885
	foreach ($context['template_layers'] as $layer)
3886
	{
3887
		loadSubTemplate($layer . '_above', true);
3888
3889
		// May seem contrived, but this is done in case the body and main layer aren't there...
3890
		if (in_array($layer, array('body', 'main')) && allowedTo('admin_forum') && !$user_info['is_guest'] && !$checked_securityFiles)
3891
		{
3892
			$checked_securityFiles = true;
3893
3894
			$securityFiles = array('install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~');
3895
3896
			// Add your own files.
3897
			call_integration_hook('integrate_security_files', array(&$securityFiles));
3898
3899
			foreach ($securityFiles as $i => $securityFile)
3900
			{
3901
				if (!file_exists($boarddir . '/' . $securityFile))
3902
					unset($securityFiles[$i]);
3903
			}
3904
3905
			// We are already checking so many files...just few more doesn't make any difference! :P
3906
			if (!empty($modSettings['currentAttachmentUploadDir']))
3907
				$path = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
3908
3909
			else
3910
				$path = $modSettings['attachmentUploadDir'];
3911
3912
			secureDirectory($path, true);
3913
			secureDirectory($cachedir);
3914
3915
			// If agreement is enabled, at least the english version shall exists
3916
			if ($modSettings['requireAgreement'])
3917
				$agreement = !file_exists($boarddir . '/agreement.txt');
3918
3919
			if (!empty($securityFiles) || (!empty($cache_enable) && !is_writable($cachedir)) || !empty($agreement))
3920
			{
3921
				echo '
3922
		<div class="errorbox">
3923
			<p class="alert">!!</p>
3924
			<h3>', empty($securityFiles) ? $txt['generic_warning'] : $txt['security_risk'], '</h3>
3925
			<p>';
3926
3927
				foreach ($securityFiles as $securityFile)
3928
				{
3929
					echo '
3930
				', $txt['not_removed'], '<strong>', $securityFile, '</strong>!<br>';
3931
3932
					if ($securityFile == 'Settings.php~' || $securityFile == 'Settings_bak.php~')
3933
						echo '
3934
				', sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)), '<br>';
3935
				}
3936
3937
				if (!empty($cache_enable) && !is_writable($cachedir))
3938
					echo '
3939
				<strong>', $txt['cache_writable'], '</strong><br>';
3940
3941
				if (!empty($agreement))
3942
					echo '
3943
				<strong>', $txt['agreement_missing'], '</strong><br>';
3944
3945
				echo '
3946
			</p>
3947
		</div>';
3948
			}
3949
		}
3950
		// If the user is banned from posting inform them of it.
3951
		elseif (in_array($layer, array('main', 'body')) && isset($_SESSION['ban']['cannot_post']) && !$showed_banned)
3952
		{
3953
			$showed_banned = true;
3954
			echo '
3955
				<div class="windowbg alert" style="margin: 2ex; padding: 2ex; border: 2px dashed red;">
3956
					', sprintf($txt['you_are_post_banned'], $user_info['is_guest'] ? $txt['guest_title'] : $user_info['name']);
3957
3958
			if (!empty($_SESSION['ban']['cannot_post']['reason']))
3959
				echo '
3960
					<div style="padding-left: 4ex; padding-top: 1ex;">', $_SESSION['ban']['cannot_post']['reason'], '</div>';
3961
3962
			if (!empty($_SESSION['ban']['expire_time']))
3963
				echo '
3964
					<div>', sprintf($txt['your_ban_expires'], timeformat($_SESSION['ban']['expire_time'], false)), '</div>';
3965
			else
3966
				echo '
3967
					<div>', $txt['your_ban_expires_never'], '</div>';
3968
3969
			echo '
3970
				</div>';
3971
		}
3972
	}
3973
}
3974
3975
/**
3976
 * Show the copyright.
3977
 */
3978
function theme_copyright()
3979
{
3980
	global $forum_copyright;
3981
3982
	// Don't display copyright for things like SSI.
3983
	if (SMF !== 1)
0 ignored issues
show
introduced by
The condition SMF !== 1 is always true.
Loading history...
3984
		return;
3985
3986
	// Put in the version...
3987
	printf($forum_copyright, SMF_FULL_VERSION, SMF_SOFTWARE_YEAR);
3988
}
3989
3990
/**
3991
 * The template footer
3992
 */
3993
function template_footer()
3994
{
3995
	global $context, $modSettings, $db_count;
3996
3997
	// Show the load time?  (only makes sense for the footer.)
3998
	$context['show_load_time'] = !empty($modSettings['timeLoadPageEnable']);
3999
	$context['load_time'] = round(microtime(true) - TIME_START, 3);
4000
	$context['load_queries'] = $db_count;
4001
4002
	if (!empty($context['template_layers']) && is_array($context['template_layers']))
4003
		foreach (array_reverse($context['template_layers']) as $layer)
4004
			loadSubTemplate($layer . '_below', true);
4005
}
4006
4007
/**
4008
 * Output the Javascript files
4009
 * 	- tabbing in this function is to make the HTML source look good and proper
4010
 *  - if deferred is set function will output all JS set to load at page end
4011
 *
4012
 * @param bool $do_deferred If true will only output the deferred JS (the stuff that goes right before the closing body tag)
4013
 */
4014
function template_javascript($do_deferred = false)
4015
{
4016
	global $context, $modSettings, $settings;
4017
4018
	// Use this hook to minify/optimize Javascript files and vars
4019
	call_integration_hook('integrate_pre_javascript_output', array(&$do_deferred));
4020
4021
	$toMinify = array(
4022
		'standard' => array(),
4023
		'defer' => array(),
4024
		'async' => array(),
4025
	);
4026
4027
	// Ouput the declared Javascript variables.
4028
	if (!empty($context['javascript_vars']) && !$do_deferred)
4029
	{
4030
		echo '
4031
	<script>';
4032
4033
		foreach ($context['javascript_vars'] as $key => $value)
4034
		{
4035
			if (empty($value))
4036
			{
4037
				echo '
4038
		var ', $key, ';';
4039
			}
4040
			else
4041
			{
4042
				echo '
4043
		var ', $key, ' = ', $value, ';';
4044
			}
4045
		}
4046
4047
		echo '
4048
	</script>';
4049
	}
4050
4051
	// In the dark days before HTML5, deferred JS files needed to be loaded at the end of the body.
4052
	// Now we load them in the head and use 'async' and/or 'defer' attributes. Much better performance.
4053
	if (!$do_deferred)
4054
	{
4055
		// While we have JavaScript files to place in the template.
4056
		foreach ($context['javascript_files'] as $id => $js_file)
4057
		{
4058
			// Last minute call! allow theme authors to disable single files.
4059
			if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
4060
				continue;
4061
4062
			// By default files don't get minimized unless the file explicitly says so!
4063
			if (!empty($js_file['options']['minimize']) && !empty($modSettings['minimize_files']))
4064
			{
4065
				if (!empty($js_file['options']['async']))
4066
					$toMinify['async'][] = $js_file;
4067
4068
				elseif (!empty($js_file['options']['defer']))
4069
					$toMinify['defer'][] = $js_file;
4070
4071
				else
4072
					$toMinify['standard'][] = $js_file;
4073
4074
				// Grab a random seed.
4075
				if (!isset($minSeed) && isset($js_file['options']['seed']))
4076
					$minSeed = $js_file['options']['seed'];
4077
			}
4078
4079
			else
4080
			{
4081
				echo '
4082
	<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' : '';
4083
4084
				if (!empty($js_file['options']['attributes']))
4085
					foreach ($js_file['options']['attributes'] as $key => $value)
4086
					{
4087
						if (is_bool($value))
4088
							echo !empty($value) ? ' ' . $key : '';
4089
4090
						else
4091
							echo ' ', $key, '="', $value, '"';
4092
					}
4093
4094
				echo '></script>';
4095
			}
4096
		}
4097
4098
		foreach ($toMinify as $js_files)
4099
		{
4100
			if (!empty($js_files))
4101
			{
4102
				$result = custMinify($js_files, 'js');
4103
4104
				$minSuccessful = array_keys($result) === array('smf_minified');
4105
4106
				foreach ($result as $minFile)
4107
					echo '
4108
	<script src="', $minFile['fileUrl'], $minSuccessful && isset($minSeed) ? $minSeed : '', '"', !empty($minFile['options']['async']) ? ' async' : '', !empty($minFile['options']['defer']) ? ' defer' : '', '></script>';
4109
			}
4110
		}
4111
	}
4112
4113
	// Inline JavaScript - Actually useful some times!
4114
	if (!empty($context['javascript_inline']))
4115
	{
4116
		if (!empty($context['javascript_inline']['defer']) && $do_deferred)
4117
		{
4118
			echo '
4119
<script>
4120
window.addEventListener("DOMContentLoaded", function() {';
4121
4122
			foreach ($context['javascript_inline']['defer'] as $js_code)
4123
				echo $js_code;
4124
4125
			echo '
4126
});
4127
</script>';
4128
		}
4129
4130
		if (!empty($context['javascript_inline']['standard']) && !$do_deferred)
4131
		{
4132
			echo '
4133
	<script>';
4134
4135
			foreach ($context['javascript_inline']['standard'] as $js_code)
4136
				echo $js_code;
4137
4138
			echo '
4139
	</script>';
4140
		}
4141
	}
4142
}
4143
4144
/**
4145
 * Output the CSS files
4146
 */
4147
function template_css()
4148
{
4149
	global $context, $db_show_debug, $boardurl, $settings, $modSettings;
4150
4151
	// Use this hook to minify/optimize CSS files
4152
	call_integration_hook('integrate_pre_css_output');
4153
4154
	$toMinify = array();
4155
	$normal = array();
4156
4157
	usort($context['css_files'], function ($a, $b)
4158
	{
4159
		return $a['options']['order_pos'] < $b['options']['order_pos'] ? -1 : ($a['options']['order_pos'] > $b['options']['order_pos'] ? 1 : 0);
4160
	});
4161
	foreach ($context['css_files'] as $id => $file)
4162
	{
4163
		// Last minute call! allow theme authors to disable single files.
4164
		if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
4165
			continue;
4166
4167
		// Files are minimized unless they explicitly opt out.
4168
		if (!isset($file['options']['minimize']))
4169
			$file['options']['minimize'] = true;
4170
4171
		if (!empty($file['options']['minimize']) && !empty($modSettings['minimize_files']) && !isset($_REQUEST['normalcss']))
4172
		{
4173
			$toMinify[] = $file;
4174
4175
			// Grab a random seed.
4176
			if (!isset($minSeed) && isset($file['options']['seed']))
4177
				$minSeed = $file['options']['seed'];
4178
		}
4179
		else
4180
			$normal[] = array(
4181
				'url' => $file['fileUrl'] . (isset($file['options']['seed']) ? $file['options']['seed'] : ''),
4182
				'attributes' => !empty($file['options']['attributes']) ? $file['options']['attributes'] : array()
4183
			);
4184
	}
4185
4186
	if (!empty($toMinify))
4187
	{
4188
		$result = custMinify($toMinify, 'css');
4189
4190
		$minSuccessful = array_keys($result) === array('smf_minified');
4191
4192
		foreach ($result as $minFile)
4193
			echo '
4194
	<link rel="stylesheet" href="', $minFile['fileUrl'], $minSuccessful && isset($minSeed) ? $minSeed : '', '">';
4195
	}
4196
4197
	// Print the rest after the minified files.
4198
	if (!empty($normal))
4199
		foreach ($normal as $nf)
4200
		{
4201
			echo '
4202
	<link rel="stylesheet" href="', $nf['url'], '"';
4203
4204
			if (!empty($nf['attributes']))
4205
				foreach ($nf['attributes'] as $key => $value)
4206
				{
4207
					if (is_bool($value))
4208
						echo !empty($value) ? ' ' . $key : '';
4209
					else
4210
						echo ' ', $key, '="', $value, '"';
4211
				}
4212
4213
			echo '>';
4214
		}
4215
4216
	if ($db_show_debug === true)
4217
	{
4218
		// Try to keep only what's useful.
4219
		$repl = array($boardurl . '/Themes/' => '', $boardurl . '/' => '');
4220
		foreach ($context['css_files'] as $file)
4221
			$context['debug']['sheets'][] = strtr($file['fileName'], $repl);
4222
	}
4223
4224
	if (!empty($context['css_header']))
4225
	{
4226
		echo '
4227
	<style>';
4228
4229
		foreach ($context['css_header'] as $css)
4230
			echo $css . '
4231
	';
4232
4233
		echo '
4234
	</style>';
4235
	}
4236
}
4237
4238
/**
4239
 * Get an array of previously defined files and adds them to our main minified files.
4240
 * Sets a one day cache to avoid re-creating a file on every request.
4241
 *
4242
 * @param array $data The files to minify.
4243
 * @param string $type either css or js.
4244
 * @return array Info about the minified file, or about the original files if the minify process failed.
4245
 */
4246
function custMinify($data, $type)
4247
{
4248
	global $settings, $txt;
4249
4250
	$types = array('css', 'js');
4251
	$type = !empty($type) && in_array($type, $types) ? $type : false;
4252
	$data = is_array($data) ? $data : array();
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
4253
4254
	if (empty($type) || empty($data))
4255
		return $data;
4256
4257
	// Different pages include different files, so we use a hash to label the different combinations
4258
	$hash = md5(implode(' ', array_map(function($file)
4259
	{
4260
		return $file['filePath'] . '-' . $file['mtime'];
4261
	}, $data)));
4262
4263
	// Is this a deferred or asynchronous JavaScript file?
4264
	$async = $type === 'js';
4265
	$defer = $type === 'js';
4266
	if ($type === 'js')
4267
	{
4268
		foreach ($data as $id => $file)
4269
		{
4270
			// A minified script should only be loaded asynchronously if all its components wanted to be.
4271
			if (empty($file['options']['async']))
4272
				$async = false;
4273
4274
			// A minified script should only be deferred if all its components wanted to be.
4275
			if (empty($file['options']['defer']))
4276
				$defer = false;
4277
		}
4278
	}
4279
4280
	// Did we already do this?
4281
	$minified_file = $settings['theme_dir'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/minified_' . $hash . '.' . $type;
4282
	$already_exists = file_exists($minified_file);
4283
4284
	// Already done?
4285
	if ($already_exists)
4286
	{
4287
		return array('smf_minified' => array(
4288
			'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file),
4289
			'filePath' => $minified_file,
4290
			'fileName' => basename($minified_file),
4291
			'options' => array('async' => !empty($async), 'defer' => !empty($defer)),
4292
		));
4293
	}
4294
	// File has to exist. If it doesn't, try to create it.
4295
	elseif (@fopen($minified_file, 'w') === false || !smf_chmod($minified_file))
4296
	{
4297
		loadLanguage('Errors');
4298
		log_error(sprintf($txt['file_not_created'], $minified_file), 'general');
4299
4300
		// The process failed, so roll back to print each individual file.
4301
		return $data;
4302
	}
4303
4304
	// No namespaces, sorry!
4305
	$classType = 'MatthiasMullie\\Minify\\' . strtoupper($type);
4306
4307
	$minifier = new $classType();
4308
4309
	foreach ($data as $id => $file)
4310
	{
4311
		$toAdd = !empty($file['filePath']) && file_exists($file['filePath']) ? $file['filePath'] : false;
4312
4313
		// The file couldn't be located so it won't be added. Log this error.
4314
		if (empty($toAdd))
4315
		{
4316
			loadLanguage('Errors');
4317
			log_error(sprintf($txt['file_minimize_fail'], !empty($file['fileName']) ? $file['fileName'] : $id), 'general');
4318
			continue;
4319
		}
4320
4321
		// Add this file to the list.
4322
		$minifier->add($toAdd);
4323
	}
4324
4325
	// Create the file.
4326
	$minifier->minify($minified_file);
4327
	unset($minifier);
4328
	clearstatcache();
4329
4330
	// Minify process failed.
4331
	if (!filesize($minified_file))
4332
	{
4333
		loadLanguage('Errors');
4334
		log_error(sprintf($txt['file_not_created'], $minified_file), 'general');
4335
4336
		// The process failed so roll back to print each individual file.
4337
		return $data;
4338
	}
4339
4340
	return array('smf_minified' => array(
4341
		'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file),
4342
		'filePath' => $minified_file,
4343
		'fileName' => basename($minified_file),
4344
		'options' => array('async' => $async, 'defer' => $defer),
4345
	));
4346
}
4347
4348
/**
4349
 * Clears out old minimized CSS and JavaScript files and ensures $modSettings['browser_cache'] is up to date
4350
 */
4351
function deleteAllMinified()
4352
{
4353
	global $smcFunc, $txt, $modSettings;
4354
4355
	$not_deleted = array();
4356
	$most_recent = 0;
4357
4358
	// Kinda sucks that we need to do another query to get all the theme dirs, but c'est la vie.
4359
	$request = $smcFunc['db_query']('', '
4360
		SELECT id_theme AS id, value AS dir
4361
		FROM {db_prefix}themes
4362
		WHERE variable = {string:var}',
4363
		array(
4364
			'var' => 'theme_dir',
4365
		)
4366
	);
4367
	while ($theme = $smcFunc['db_fetch_assoc']($request))
4368
	{
4369
		foreach (array('css', 'js') as $type)
4370
		{
4371
			foreach (glob(rtrim($theme['dir'], '/') . '/' . ($type == 'css' ? 'css' : 'scripts') . '/*.' . $type) as $filename)
4372
			{
4373
				// We want to find the most recent mtime of non-minified files
4374
				if (strpos(pathinfo($filename, PATHINFO_BASENAME), 'minified') === false)
4375
					$most_recent = max($modSettings['browser_cache'], (int) @filemtime($filename));
4376
4377
				// Try to delete minified files. Add them to our error list if that fails.
4378
				elseif (!@unlink($filename))
4379
					$not_deleted[] = $filename;
4380
			}
4381
		}
4382
	}
4383
	$smcFunc['db_free_result']($request);
4384
4385
	// This setting tracks the most recent modification time of any of our CSS and JS files
4386
	if ($most_recent > $modSettings['browser_cache'])
4387
		updateSettings(array('browser_cache' => $most_recent));
4388
4389
	// If any of the files could not be deleted, log an error about it.
4390
	if (!empty($not_deleted))
4391
	{
4392
		loadLanguage('Errors');
4393
		log_error(sprintf($txt['unlink_minimized_fail'], implode('<br>', $not_deleted)), 'general');
4394
	}
4395
}
4396
4397
/**
4398
 * Get an attachment's encrypted filename. If $new is true, won't check for file existence.
4399
 *
4400
 * @todo this currently returns the hash if new, and the full filename otherwise.
4401
 * Something messy like that.
4402
 * @todo and of course everything relies on this behavior and work around it. :P.
4403
 * Converters included.
4404
 *
4405
 * @param string $filename The name of the file
4406
 * @param int $attachment_id The ID of the attachment
4407
 * @param string|null $dir Which directory it should be in (null to use current one)
4408
 * @param bool $new Whether this is a new attachment
4409
 * @param string $file_hash The file hash
4410
 * @return string The path to the file
4411
 */
4412
function getAttachmentFilename($filename, $attachment_id, $dir = null, $new = false, $file_hash = '')
4413
{
4414
	global $modSettings, $smcFunc;
4415
4416
	// Just make up a nice hash...
4417
	if ($new)
4418
		return sha1(md5($filename . time()) . mt_rand());
4419
4420
	// Just make sure that attachment id is only a int
4421
	$attachment_id = (int) $attachment_id;
4422
4423
	// Grab the file hash if it wasn't added.
4424
	// Left this for legacy.
4425
	if ($file_hash === '')
4426
	{
4427
		$request = $smcFunc['db_query']('', '
4428
			SELECT file_hash
4429
			FROM {db_prefix}attachments
4430
			WHERE id_attach = {int:id_attach}',
4431
			array(
4432
				'id_attach' => $attachment_id,
4433
			)
4434
		);
4435
4436
		if ($smcFunc['db_num_rows']($request) === 0)
4437
			return false;
4438
4439
		list ($file_hash) = $smcFunc['db_fetch_row']($request);
4440
		$smcFunc['db_free_result']($request);
4441
	}
4442
4443
	// Still no hash? mmm...
4444
	if (empty($file_hash))
4445
		$file_hash = sha1(md5($filename . time()) . mt_rand());
4446
4447
	// Are we using multiple directories?
4448
	if (is_array($modSettings['attachmentUploadDir']))
4449
		$path = $modSettings['attachmentUploadDir'][$dir];
4450
4451
	else
4452
		$path = $modSettings['attachmentUploadDir'];
4453
4454
	return $path . '/' . $attachment_id . '_' . $file_hash . '.dat';
4455
}
4456
4457
/**
4458
 * Convert a single IP to a ranged IP.
4459
 * internal function used to convert a user-readable format to a format suitable for the database.
4460
 *
4461
 * @param string $fullip The full IP
4462
 * @return array An array of IP parts
4463
 */
4464
function ip2range($fullip)
4465
{
4466
	// Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.)
4467
	if ($fullip == 'unknown')
4468
		$fullip = '255.255.255.255';
4469
4470
	$ip_parts = explode('-', $fullip);
4471
	$ip_array = array();
4472
4473
	// if ip 22.12.31.21
4474
	if (count($ip_parts) == 1 && isValidIP($fullip))
4475
	{
4476
		$ip_array['low'] = $fullip;
4477
		$ip_array['high'] = $fullip;
4478
		return $ip_array;
4479
	} // if ip 22.12.* -> 22.12.* - 22.12.*
4480
	elseif (count($ip_parts) == 1)
4481
	{
4482
		$ip_parts[0] = $fullip;
4483
		$ip_parts[1] = $fullip;
4484
	}
4485
4486
	// if ip 22.12.31.21-12.21.31.21
4487
	if (count($ip_parts) == 2 && isValidIP($ip_parts[0]) && isValidIP($ip_parts[1]))
4488
	{
4489
		$ip_array['low'] = $ip_parts[0];
4490
		$ip_array['high'] = $ip_parts[1];
4491
		return $ip_array;
4492
	}
4493
	elseif (count($ip_parts) == 2) // if ip 22.22.*-22.22.*
4494
	{
4495
		$valid_low = isValidIP($ip_parts[0]);
4496
		$valid_high = isValidIP($ip_parts[1]);
4497
		$count = 0;
4498
		$mode = (preg_match('/:/', $ip_parts[0]) > 0 ? ':' : '.');
4499
		$max = ($mode == ':' ? 'ffff' : '255');
4500
		$min = 0;
4501
		if (!$valid_low)
4502
		{
4503
			$ip_parts[0] = preg_replace('/\*/', '0', $ip_parts[0]);
4504
			$valid_low = isValidIP($ip_parts[0]);
4505
			while (!$valid_low)
4506
			{
4507
				$ip_parts[0] .= $mode . $min;
4508
				$valid_low = isValidIP($ip_parts[0]);
4509
				$count++;
4510
				if ($count > 9) break;
4511
			}
4512
		}
4513
4514
		$count = 0;
4515
		if (!$valid_high)
4516
		{
4517
			$ip_parts[1] = preg_replace('/\*/', $max, $ip_parts[1]);
4518
			$valid_high = isValidIP($ip_parts[1]);
4519
			while (!$valid_high)
4520
			{
4521
				$ip_parts[1] .= $mode . $max;
4522
				$valid_high = isValidIP($ip_parts[1]);
4523
				$count++;
4524
				if ($count > 9) break;
4525
			}
4526
		}
4527
4528
		if ($valid_high && $valid_low)
4529
		{
4530
			$ip_array['low'] = $ip_parts[0];
4531
			$ip_array['high'] = $ip_parts[1];
4532
		}
4533
	}
4534
4535
	return $ip_array;
4536
}
4537
4538
/**
4539
 * Lookup an IP; try shell_exec first because we can do a timeout on it.
4540
 *
4541
 * @param string $ip The IP to get the hostname from
4542
 * @return string The hostname
4543
 */
4544
function host_from_ip($ip)
4545
{
4546
	global $modSettings;
4547
4548
	if (($host = cache_get_data('hostlookup-' . $ip, 600)) !== null)
4549
		return $host;
4550
	$t = microtime(true);
4551
4552
	// Try the Linux host command, perhaps?
4553
	if (!isset($host) && (strpos(strtolower(PHP_OS), 'win') === false || strpos(strtolower(PHP_OS), 'darwin') !== false) && mt_rand(0, 1) == 1)
4554
	{
4555
		if (!isset($modSettings['host_to_dis']))
4556
			$test = @shell_exec('host -W 1 ' . @escapeshellarg($ip));
4557
		else
4558
			$test = @shell_exec('host ' . @escapeshellarg($ip));
4559
4560
		// Did host say it didn't find anything?
4561
		if (strpos($test, 'not found') !== false)
4562
			$host = '';
4563
		// Invalid server option?
4564
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
4565
			updateSettings(array('host_to_dis' => 1));
4566
		// Maybe it found something, after all?
4567
		elseif (preg_match('~\s([^\s]+?)\.\s~', $test, $match) == 1)
4568
			$host = $match[1];
4569
	}
4570
4571
	// This is nslookup; usually only Windows, but possibly some Unix?
4572
	if (!isset($host) && stripos(PHP_OS, 'win') !== false && strpos(strtolower(PHP_OS), 'darwin') === false && mt_rand(0, 1) == 1)
4573
	{
4574
		$test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
4575
		if (strpos($test, 'Non-existent domain') !== false)
4576
			$host = '';
4577
		elseif (preg_match('~Name:\s+([^\s]+)~', $test, $match) == 1)
4578
			$host = $match[1];
4579
	}
4580
4581
	// This is the last try :/.
4582
	if (!isset($host) || $host === false)
4583
		$host = @gethostbyaddr($ip);
4584
4585
	// It took a long time, so let's cache it!
4586
	if (microtime(true) - $t > 0.5)
4587
		cache_put_data('hostlookup-' . $ip, $host, 600);
4588
4589
	return $host;
4590
}
4591
4592
/**
4593
 * Chops a string into words and prepares them to be inserted into (or searched from) the database.
4594
 *
4595
 * @param string $text The text to split into words
4596
 * @param int $max_chars The maximum number of characters per word
4597
 * @param bool $encrypt Whether to encrypt the results
4598
 * @return array An array of ints or words depending on $encrypt
4599
 */
4600
function text2words($text, $max_chars = 20, $encrypt = false)
4601
{
4602
	global $smcFunc, $context;
4603
4604
	// Upgrader may be working on old DBs...
4605
	if (!isset($context['utf8']))
4606
		$context['utf8'] = false;
4607
4608
	// Step 1: Remove entities/things we don't consider words:
4609
	$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>' => ' ')));
4610
4611
	// Step 2: Entities we left to letters, where applicable, lowercase.
4612
	$words = un_htmlspecialchars($smcFunc['strtolower']($words));
4613
4614
	// Step 3: Ready to split apart and index!
4615
	$words = explode(' ', $words);
4616
4617
	if ($encrypt)
4618
	{
4619
		$possible_chars = array_flip(array_merge(range(46, 57), range(65, 90), range(97, 122)));
4620
		$returned_ints = array();
4621
		foreach ($words as $word)
4622
		{
4623
			if (($word = trim($word, '-_\'')) !== '')
4624
			{
4625
				$encrypted = substr(crypt($word, 'uk'), 2, $max_chars);
4626
				$total = 0;
4627
				for ($i = 0; $i < $max_chars; $i++)
4628
					$total += $possible_chars[ord($encrypted[$i])] * pow(63, $i);
4629
				$returned_ints[] = $max_chars == 4 ? min($total, 16777215) : $total;
4630
			}
4631
		}
4632
		return array_unique($returned_ints);
4633
	}
4634
	else
4635
	{
4636
		// Trim characters before and after and add slashes for database insertion.
4637
		$returned_words = array();
4638
		foreach ($words as $word)
4639
			if (($word = trim($word, '-_\'')) !== '')
4640
				$returned_words[] = $max_chars === null ? $word : substr($word, 0, $max_chars);
4641
4642
		// Filter out all words that occur more than once.
4643
		return array_unique($returned_words);
4644
	}
4645
}
4646
4647
/**
4648
 * Creates an image/text button
4649
 *
4650
 * @deprecated since 2.1
4651
 * @param string $name The name of the button (should be a main_icons class or the name of an image)
4652
 * @param string $alt The alt text
4653
 * @param string $label The $txt string to use as the label
4654
 * @param string $custom Custom text/html to add to the img tag (only when using an actual image)
4655
 * @param boolean $force_use Whether to force use of this when template_create_button is available
4656
 * @return string The HTML to display the button
4657
 */
4658
function create_button($name, $alt, $label = '', $custom = '', $force_use = false)
4659
{
4660
	global $settings, $txt;
4661
4662
	// Does the current loaded theme have this and we are not forcing the usage of this function?
4663
	if (function_exists('template_create_button') && !$force_use)
4664
		return template_create_button($name, $alt, $label = '', $custom = '');
4665
4666
	if (!$settings['use_image_buttons'])
4667
		return $txt[$alt];
4668
	elseif (!empty($settings['use_buttons']))
4669
		return '<span class="main_icons ' . $name . '" alt="' . $txt[$alt] . '"></span>' . ($label != '' ? '&nbsp;<strong>' . $txt[$label] . '</strong>' : '');
4670
	else
4671
		return '<img src="' . $settings['lang_images_url'] . '/' . $name . '" alt="' . $txt[$alt] . '" ' . $custom . '>';
4672
}
4673
4674
/**
4675
 * Sets up all of the top menu buttons
4676
 * Saves them in the cache if it is available and on
4677
 * Places the results in $context
4678
 */
4679
function setupMenuContext()
4680
{
4681
	global $context, $modSettings, $user_info, $txt, $scripturl, $sourcedir, $settings, $smcFunc, $cache_enable;
4682
4683
	// Set up the menu privileges.
4684
	$context['allow_search'] = !empty($modSettings['allow_guestAccess']) ? allowedTo('search_posts') : (!$user_info['is_guest'] && allowedTo('search_posts'));
4685
	$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'));
4686
4687
	$context['allow_memberlist'] = allowedTo('view_mlist');
4688
	$context['allow_calendar'] = allowedTo('calendar_view') && !empty($modSettings['cal_enabled']);
4689
	$context['allow_moderation_center'] = $context['user']['can_mod'];
4690
	$context['allow_pm'] = allowedTo('pm_read');
4691
4692
	$cacheTime = $modSettings['lastActive'] * 60;
4693
4694
	// Initial "can you post an event in the calendar" option - but this might have been set in the calendar already.
4695
	if (!isset($context['allow_calendar_event']))
4696
	{
4697
		$context['allow_calendar_event'] = $context['allow_calendar'] && allowedTo('calendar_post');
4698
4699
		// If you don't allow events not linked to posts and you're not an admin, we have more work to do...
4700
		if ($context['allow_calendar'] && $context['allow_calendar_event'] && empty($modSettings['cal_allow_unlinked']) && !$user_info['is_admin'])
4701
		{
4702
			$boards_can_post = boardsAllowedTo('post_new');
4703
			$context['allow_calendar_event'] &= !empty($boards_can_post);
4704
		}
4705
	}
4706
4707
	// There is some menu stuff we need to do if we're coming at this from a non-guest perspective.
4708
	if (!$context['user']['is_guest'])
4709
	{
4710
		addInlineJavaScript('
4711
	var user_menus = new smc_PopupMenu();
4712
	user_menus.add("profile", "' . $scripturl . '?action=profile;area=popup");
4713
	user_menus.add("alerts", "' . $scripturl . '?action=profile;area=alerts_popup;u=' . $context['user']['id'] . '");', true);
4714
		if ($context['allow_pm'])
4715
			addInlineJavaScript('
4716
	user_menus.add("pm", "' . $scripturl . '?action=pm;sa=popup");', true);
4717
4718
		if (!empty($modSettings['enable_ajax_alerts']))
4719
		{
4720
			require_once($sourcedir . '/Subs-Notify.php');
4721
4722
			$timeout = getNotifyPrefs($context['user']['id'], 'alert_timeout', true);
4723
			$timeout = empty($timeout) ? 10000 : $timeout[$context['user']['id']]['alert_timeout'] * 1000;
4724
4725
			addInlineJavaScript('
4726
	var new_alert_title = "' . $context['forum_name'] . '";
4727
	var alert_timeout = ' . $timeout . ';');
4728
			loadJavaScriptFile('alerts.js', array('minimize' => true), 'smf_alerts');
4729
		}
4730
	}
4731
4732
	// All the buttons we can possible want and then some, try pulling the final list of buttons from cache first.
4733
	if (($menu_buttons = cache_get_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $cacheTime)) === null || time() - $cacheTime <= $modSettings['settings_updated'])
4734
	{
4735
		$buttons = array(
4736
			'home' => array(
4737
				'title' => $txt['home'],
4738
				'href' => $scripturl,
4739
				'show' => true,
4740
				'sub_buttons' => array(
4741
				),
4742
				'is_last' => $context['right_to_left'],
4743
			),
4744
			'search' => array(
4745
				'title' => $txt['search'],
4746
				'href' => $scripturl . '?action=search',
4747
				'show' => $context['allow_search'],
4748
				'sub_buttons' => array(
4749
				),
4750
			),
4751
			'admin' => array(
4752
				'title' => $txt['admin'],
4753
				'href' => $scripturl . '?action=admin',
4754
				'show' => $context['allow_admin'],
4755
				'sub_buttons' => array(
4756
					'featuresettings' => array(
4757
						'title' => $txt['modSettings_title'],
4758
						'href' => $scripturl . '?action=admin;area=featuresettings',
4759
						'show' => allowedTo('admin_forum'),
4760
					),
4761
					'packages' => array(
4762
						'title' => $txt['package'],
4763
						'href' => $scripturl . '?action=admin;area=packages',
4764
						'show' => allowedTo('admin_forum'),
4765
					),
4766
					'errorlog' => array(
4767
						'title' => $txt['errorlog'],
4768
						'href' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc',
4769
						'show' => allowedTo('admin_forum') && !empty($modSettings['enableErrorLogging']),
4770
					),
4771
					'permissions' => array(
4772
						'title' => $txt['edit_permissions'],
4773
						'href' => $scripturl . '?action=admin;area=permissions',
4774
						'show' => allowedTo('manage_permissions'),
4775
					),
4776
					'memberapprove' => array(
4777
						'title' => $txt['approve_members_waiting'],
4778
						'href' => $scripturl . '?action=admin;area=viewmembers;sa=browse;type=approve',
4779
						'show' => !empty($context['unapproved_members']),
4780
						'is_last' => true,
4781
					),
4782
				),
4783
			),
4784
			'moderate' => array(
4785
				'title' => $txt['moderate'],
4786
				'href' => $scripturl . '?action=moderate',
4787
				'show' => $context['allow_moderation_center'],
4788
				'sub_buttons' => array(
4789
					'modlog' => array(
4790
						'title' => $txt['modlog_view'],
4791
						'href' => $scripturl . '?action=moderate;area=modlog',
4792
						'show' => !empty($modSettings['modlog_enabled']) && !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
4793
					),
4794
					'poststopics' => array(
4795
						'title' => $txt['mc_unapproved_poststopics'],
4796
						'href' => $scripturl . '?action=moderate;area=postmod;sa=posts',
4797
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
4798
					),
4799
					'attachments' => array(
4800
						'title' => $txt['mc_unapproved_attachments'],
4801
						'href' => $scripturl . '?action=moderate;area=attachmod;sa=attachments',
4802
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
4803
					),
4804
					'reports' => array(
4805
						'title' => $txt['mc_reported_posts'],
4806
						'href' => $scripturl . '?action=moderate;area=reportedposts',
4807
						'show' => !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
4808
					),
4809
					'reported_members' => array(
4810
						'title' => $txt['mc_reported_members'],
4811
						'href' => $scripturl . '?action=moderate;area=reportedmembers',
4812
						'show' => allowedTo('moderate_forum'),
4813
						'is_last' => true,
4814
					)
4815
				),
4816
			),
4817
			'calendar' => array(
4818
				'title' => $txt['calendar'],
4819
				'href' => $scripturl . '?action=calendar',
4820
				'show' => $context['allow_calendar'],
4821
				'sub_buttons' => array(
4822
					'view' => array(
4823
						'title' => $txt['calendar_menu'],
4824
						'href' => $scripturl . '?action=calendar',
4825
						'show' => $context['allow_calendar_event'],
4826
					),
4827
					'post' => array(
4828
						'title' => $txt['calendar_post_event'],
4829
						'href' => $scripturl . '?action=calendar;sa=post',
4830
						'show' => $context['allow_calendar_event'],
4831
						'is_last' => true,
4832
					),
4833
				),
4834
			),
4835
			'mlist' => array(
4836
				'title' => $txt['members_title'],
4837
				'href' => $scripturl . '?action=mlist',
4838
				'show' => $context['allow_memberlist'],
4839
				'sub_buttons' => array(
4840
					'mlist_view' => array(
4841
						'title' => $txt['mlist_menu_view'],
4842
						'href' => $scripturl . '?action=mlist',
4843
						'show' => true,
4844
					),
4845
					'mlist_search' => array(
4846
						'title' => $txt['mlist_search'],
4847
						'href' => $scripturl . '?action=mlist;sa=search',
4848
						'show' => true,
4849
						'is_last' => true,
4850
					),
4851
				),
4852
				'is_last' => !$context['right_to_left'] && (!$user_info['is_guest'] || !$context['can_register']),
4853
			),
4854
			'signup' => array(
4855
				'title' => $txt['register'],
4856
				'href' => $scripturl . '?action=signup',
4857
				'show' => $user_info['is_guest'] && $context['can_register'],
4858
				'sub_buttons' => array(
4859
				),
4860
				'is_last' => !$context['right_to_left'],
4861
			),
4862
		);
4863
4864
		// Allow editing menu buttons easily.
4865
		call_integration_hook('integrate_menu_buttons', array(&$buttons));
4866
4867
		// Now we put the buttons in the context so the theme can use them.
4868
		$menu_buttons = array();
4869
		foreach ($buttons as $act => $button)
4870
			if (!empty($button['show']))
4871
			{
4872
				$button['active_button'] = false;
4873
4874
				// This button needs some action.
4875
				if (isset($button['action_hook']))
4876
					$needs_action_hook = true;
4877
4878
				// Make sure the last button truly is the last button.
4879
				if (!empty($button['is_last']))
4880
				{
4881
					if (isset($last_button))
4882
						unset($menu_buttons[$last_button]['is_last']);
4883
					$last_button = $act;
4884
				}
4885
4886
				// Go through the sub buttons if there are any.
4887
				if (!empty($button['sub_buttons']))
4888
					foreach ($button['sub_buttons'] as $key => $subbutton)
4889
					{
4890
						if (empty($subbutton['show']))
4891
							unset($button['sub_buttons'][$key]);
4892
4893
						// 2nd level sub buttons next...
4894
						if (!empty($subbutton['sub_buttons']))
4895
						{
4896
							foreach ($subbutton['sub_buttons'] as $key2 => $sub_button2)
4897
							{
4898
								if (empty($sub_button2['show']))
4899
									unset($button['sub_buttons'][$key]['sub_buttons'][$key2]);
4900
							}
4901
						}
4902
					}
4903
4904
				// Does this button have its own icon?
4905
				if (isset($button['icon']) && file_exists($settings['theme_dir'] . '/images/' . $button['icon']))
4906
					$button['icon'] = '<img src="' . $settings['images_url'] . '/' . $button['icon'] . '" alt="">';
4907
				elseif (isset($button['icon']) && file_exists($settings['default_theme_dir'] . '/images/' . $button['icon']))
4908
					$button['icon'] = '<img src="' . $settings['default_images_url'] . '/' . $button['icon'] . '" alt="">';
4909
				elseif (isset($button['icon']))
4910
					$button['icon'] = '<span class="main_icons ' . $button['icon'] . '"></span>';
4911
				else
4912
					$button['icon'] = '<span class="main_icons ' . $act . '"></span>';
4913
4914
				$menu_buttons[$act] = $button;
4915
			}
4916
4917
		if (!empty($cache_enable) && $cache_enable >= 2)
4918
			cache_put_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $menu_buttons, $cacheTime);
4919
	}
4920
4921
	$context['menu_buttons'] = $menu_buttons;
4922
4923
	// Logging out requires the session id in the url.
4924
	if (isset($context['menu_buttons']['logout']))
4925
		$context['menu_buttons']['logout']['href'] = sprintf($context['menu_buttons']['logout']['href'], $context['session_var'], $context['session_id']);
4926
4927
	// Figure out which action we are doing so we can set the active tab.
4928
	// Default to home.
4929
	$current_action = 'home';
4930
4931
	if (isset($context['menu_buttons'][$context['current_action']]))
4932
		$current_action = $context['current_action'];
4933
	elseif ($context['current_action'] == 'search2')
4934
		$current_action = 'search';
4935
	elseif ($context['current_action'] == 'theme')
4936
		$current_action = isset($_REQUEST['sa']) && $_REQUEST['sa'] == 'pick' ? 'profile' : 'admin';
4937
	elseif ($context['current_action'] == 'register2')
4938
		$current_action = 'register';
4939
	elseif ($context['current_action'] == 'login2' || ($user_info['is_guest'] && $context['current_action'] == 'reminder'))
4940
		$current_action = 'login';
4941
	elseif ($context['current_action'] == 'groups' && $context['allow_moderation_center'])
4942
		$current_action = 'moderate';
4943
4944
	// There are certain exceptions to the above where we don't want anything on the menu highlighted.
4945
	if ($context['current_action'] == 'profile' && !empty($context['user']['is_owner']))
4946
	{
4947
		$current_action = !empty($_GET['area']) && $_GET['area'] == 'showalerts' ? 'self_alerts' : 'self_profile';
4948
		$context[$current_action] = true;
4949
	}
4950
	elseif ($context['current_action'] == 'pm')
4951
	{
4952
		$current_action = 'self_pm';
4953
		$context['self_pm'] = true;
4954
	}
4955
4956
	$context['total_mod_reports'] = 0;
4957
	$context['total_admin_reports'] = 0;
4958
4959
	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']))
4960
	{
4961
		$context['total_mod_reports'] = $context['open_mod_reports'];
4962
		$context['menu_buttons']['moderate']['sub_buttons']['reports']['amt'] = $context['open_mod_reports'];
4963
	}
4964
4965
	// Show how many errors there are
4966
	if (!empty($context['menu_buttons']['admin']['sub_buttons']['errorlog']))
4967
	{
4968
		// Get an error count, if necessary
4969
		if (!isset($context['num_errors']))
4970
		{
4971
			$query = $smcFunc['db_query']('', '
4972
				SELECT COUNT(*)
4973
				FROM {db_prefix}log_errors',
4974
				array()
4975
			);
4976
4977
			list($context['num_errors']) = $smcFunc['db_fetch_row']($query);
4978
			$smcFunc['db_free_result']($query);
4979
		}
4980
4981
		if (!empty($context['num_errors']))
4982
		{
4983
			$context['total_admin_reports'] += $context['num_errors'];
4984
			$context['menu_buttons']['admin']['sub_buttons']['errorlog']['amt'] = $context['num_errors'];
4985
		}
4986
	}
4987
4988
	// Show number of reported members
4989
	if (!empty($context['open_member_reports']) && !empty($context['menu_buttons']['moderate']['sub_buttons']['reported_members']))
4990
	{
4991
		$context['total_mod_reports'] += $context['open_member_reports'];
4992
		$context['menu_buttons']['moderate']['sub_buttons']['reported_members']['amt'] = $context['open_member_reports'];
4993
	}
4994
4995
	if (!empty($context['unapproved_members']) && !empty($context['menu_buttons']['admin']))
4996
	{
4997
		$context['menu_buttons']['admin']['sub_buttons']['memberapprove']['amt'] = $context['unapproved_members'];
4998
		$context['total_admin_reports'] += $context['unapproved_members'];
4999
	}
5000
5001
	if ($context['total_admin_reports'] > 0 && !empty($context['menu_buttons']['admin']))
5002
	{
5003
		$context['menu_buttons']['admin']['amt'] = $context['total_admin_reports'];
5004
	}
5005
5006
	// Do we have any open reports?
5007
	if ($context['total_mod_reports'] > 0 && !empty($context['menu_buttons']['moderate']))
5008
	{
5009
		$context['menu_buttons']['moderate']['amt'] = $context['total_mod_reports'];
5010
	}
5011
5012
	// Not all actions are simple.
5013
	if (!empty($needs_action_hook))
5014
		call_integration_hook('integrate_current_action', array(&$current_action));
5015
5016
	if (isset($context['menu_buttons'][$current_action]))
5017
		$context['menu_buttons'][$current_action]['active_button'] = true;
5018
}
5019
5020
/**
5021
 * Generate a random seed and ensure it's stored in settings.
5022
 */
5023
function smf_seed_generator()
5024
{
5025
	updateSettings(array('rand_seed' => microtime(true)));
5026
}
5027
5028
/**
5029
 * Process functions of an integration hook.
5030
 * calls all functions of the given hook.
5031
 * supports static class method calls.
5032
 *
5033
 * @param string $hook The hook name
5034
 * @param array $parameters An array of parameters this hook implements
5035
 * @return array The results of the functions
5036
 */
5037
function call_integration_hook($hook, $parameters = array())
5038
{
5039
	global $modSettings, $settings, $boarddir, $sourcedir, $db_show_debug;
5040
	global $context, $txt;
5041
5042
	if ($db_show_debug === true)
5043
		$context['debug']['hooks'][] = $hook;
5044
5045
	// Need to have some control.
5046
	if (!isset($context['instances']))
5047
		$context['instances'] = array();
5048
5049
	$results = array();
5050
	if (empty($modSettings[$hook]))
5051
		return $results;
5052
5053
	$functions = explode(',', $modSettings[$hook]);
5054
	// Loop through each function.
5055
	foreach ($functions as $function)
5056
	{
5057
		// Hook has been marked as "disabled". Skip it!
5058
		if (strpos($function, '!') !== false)
5059
			continue;
5060
5061
		$call = call_helper($function, true);
5062
5063
		// Is it valid?
5064
		if (!empty($call))
5065
			$results[$function] = call_user_func_array($call, $parameters);
0 ignored issues
show
Bug introduced by
It seems like $call can also be of type boolean; however, parameter $function of call_user_func_array() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

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

5065
			$results[$function] = call_user_func_array(/** @scrutinizer ignore-type */ $call, $parameters);
Loading history...
5066
		// This failed, but we want to do so silently.
5067
		elseif (!empty($function) && !empty($context['ignore_hook_errors']))
5068
			return $results;
5069
		// Whatever it was suppose to call, it failed :(
5070
		elseif (!empty($function))
5071
		{
5072
			loadLanguage('Errors');
5073
5074
			// Get a full path to show on error.
5075
			if (strpos($function, '|') !== false)
5076
			{
5077
				list ($file, $string) = explode('|', $function);
5078
				$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'])));
5079
				log_error(sprintf($txt['hook_fail_call_to'], $string, $absPath), 'general');
5080
			}
5081
			// "Assume" the file resides on $boarddir somewhere...
5082
			else
5083
				log_error(sprintf($txt['hook_fail_call_to'], $function, $boarddir), 'general');
5084
		}
5085
	}
5086
5087
	return $results;
5088
}
5089
5090
/**
5091
 * Add a function for integration hook.
5092
 * does nothing if the function is already added.
5093
 *
5094
 * @param string $hook The complete hook name.
5095
 * @param string $function The function name. Can be a call to a method via Class::method.
5096
 * @param bool $permanent If true, updates the value in settings table.
5097
 * @param string $file The file. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
5098
 * @param bool $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
5099
 */
5100
function add_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
5101
{
5102
	global $smcFunc, $modSettings;
5103
5104
	// Any objects?
5105
	if ($object)
5106
		$function = $function . '#';
5107
5108
	// Any files  to load?
5109
	if (!empty($file) && is_string($file))
5110
		$function = $file . (!empty($function) ? '|' . $function : '');
5111
5112
	// Get the correct string.
5113
	$integration_call = $function;
5114
5115
	// Is it going to be permanent?
5116
	if ($permanent)
5117
	{
5118
		$request = $smcFunc['db_query']('', '
5119
			SELECT value
5120
			FROM {db_prefix}settings
5121
			WHERE variable = {string:variable}',
5122
			array(
5123
				'variable' => $hook,
5124
			)
5125
		);
5126
		list ($current_functions) = $smcFunc['db_fetch_row']($request);
5127
		$smcFunc['db_free_result']($request);
5128
5129
		if (!empty($current_functions))
5130
		{
5131
			$current_functions = explode(',', $current_functions);
5132
			if (in_array($integration_call, $current_functions))
5133
				return;
5134
5135
			$permanent_functions = array_merge($current_functions, array($integration_call));
5136
		}
5137
		else
5138
			$permanent_functions = array($integration_call);
5139
5140
		updateSettings(array($hook => implode(',', $permanent_functions)));
5141
	}
5142
5143
	// Make current function list usable.
5144
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
5145
5146
	// Do nothing, if it's already there.
5147
	if (in_array($integration_call, $functions))
5148
		return;
5149
5150
	$functions[] = $integration_call;
5151
	$modSettings[$hook] = implode(',', $functions);
5152
}
5153
5154
/**
5155
 * Remove an integration hook function.
5156
 * Removes the given function from the given hook.
5157
 * Does nothing if the function is not available.
5158
 *
5159
 * @param string $hook The complete hook name.
5160
 * @param string $function The function name. Can be a call to a method via Class::method.
5161
 * @param boolean $permanent Irrelevant for the function itself but need to declare it to match
5162
 * @param string $file The filename. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
5163
 * @param boolean $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
5164
 * @see add_integration_function
5165
 */
5166
function remove_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
5167
{
5168
	global $smcFunc, $modSettings;
5169
5170
	// Any objects?
5171
	if ($object)
5172
		$function = $function . '#';
5173
5174
	// Any files  to load?
5175
	if (!empty($file) && is_string($file))
5176
		$function = $file . '|' . $function;
5177
5178
	// Get the correct string.
5179
	$integration_call = $function;
5180
5181
	// Get the permanent functions.
5182
	$request = $smcFunc['db_query']('', '
5183
		SELECT value
5184
		FROM {db_prefix}settings
5185
		WHERE variable = {string:variable}',
5186
		array(
5187
			'variable' => $hook,
5188
		)
5189
	);
5190
	list ($current_functions) = $smcFunc['db_fetch_row']($request);
5191
	$smcFunc['db_free_result']($request);
5192
5193
	if (!empty($current_functions))
5194
	{
5195
		$current_functions = explode(',', $current_functions);
5196
5197
		if (in_array($integration_call, $current_functions))
5198
			updateSettings(array($hook => implode(',', array_diff($current_functions, array($integration_call)))));
5199
	}
5200
5201
	// Turn the function list into something usable.
5202
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
5203
5204
	// You can only remove it if it's available.
5205
	if (!in_array($integration_call, $functions))
5206
		return;
5207
5208
	$functions = array_diff($functions, array($integration_call));
5209
	$modSettings[$hook] = implode(',', $functions);
5210
}
5211
5212
/**
5213
 * Receives a string and tries to figure it out if its a method or a function.
5214
 * If a method is found, it looks for a "#" which indicates SMF should create a new instance of the given class.
5215
 * Checks the string/array for is_callable() and return false/fatal_lang_error is the given value results in a non callable string/array.
5216
 * Prepare and returns a callable depending on the type of method/function found.
5217
 *
5218
 * @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)
5219
 * @param boolean $return If true, the function will not call the function/method but instead will return the formatted string.
5220
 * @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.
5221
 */
5222
function call_helper($string, $return = false)
5223
{
5224
	global $context, $smcFunc, $txt, $db_show_debug;
5225
5226
	// Really?
5227
	if (empty($string))
5228
		return false;
5229
5230
	// An array? should be a "callable" array IE array(object/class, valid_callable).
5231
	// A closure? should be a callable one.
5232
	if (is_array($string) || $string instanceof Closure)
5233
		return $return ? $string : (is_callable($string) ? call_user_func($string) : false);
5234
5235
	// No full objects, sorry! pass a method or a property instead!
5236
	if (is_object($string))
5237
		return false;
5238
5239
	// Stay vitaminized my friends...
5240
	$string = $smcFunc['htmlspecialchars']($smcFunc['htmltrim']($string));
5241
5242
	// Is there a file to load?
5243
	$string = load_file($string);
5244
5245
	// Loaded file failed
5246
	if (empty($string))
5247
		return false;
5248
5249
	// Found a method.
5250
	if (strpos($string, '::') !== false)
5251
	{
5252
		list ($class, $method) = explode('::', $string);
5253
5254
		// Check if a new object will be created.
5255
		if (strpos($method, '#') !== false)
5256
		{
5257
			// Need to remove the # thing.
5258
			$method = str_replace('#', '', $method);
5259
5260
			// Don't need to create a new instance for every method.
5261
			if (empty($context['instances'][$class]) || !($context['instances'][$class] instanceof $class))
5262
			{
5263
				$context['instances'][$class] = new $class;
5264
5265
				// Add another one to the list.
5266
				if ($db_show_debug === true)
5267
				{
5268
					if (!isset($context['debug']['instances']))
5269
						$context['debug']['instances'] = array();
5270
5271
					$context['debug']['instances'][$class] = $class;
5272
				}
5273
			}
5274
5275
			$func = array($context['instances'][$class], $method);
5276
		}
5277
5278
		// Right then. This is a call to a static method.
5279
		else
5280
			$func = array($class, $method);
5281
	}
5282
5283
	// Nope! just a plain regular function.
5284
	else
5285
		$func = $string;
5286
5287
	// We can't call this helper, but we want to silently ignore this.
5288
	if (!is_callable($func, false, $callable_name) && !empty($context['ignore_hook_errors']))
5289
		return false;
5290
	// Right, we got what we need, time to do some checks.
5291
	elseif (!is_callable($func, false, $callable_name))
5292
	{
5293
		loadLanguage('Errors');
5294
		log_error(sprintf($txt['sub_action_fail'], $callable_name), 'general');
5295
5296
		// Gotta tell everybody.
5297
		return false;
5298
	}
5299
5300
	// Everything went better than expected.
5301
	else
5302
	{
5303
		// What are we gonna do about it?
5304
		if ($return)
5305
			return $func;
5306
5307
		// If this is a plain function, avoid the heat of calling call_user_func().
5308
		else
5309
		{
5310
			if (is_array($func))
5311
				call_user_func($func);
5312
5313
			else
5314
				$func();
5315
		}
5316
	}
5317
}
5318
5319
/**
5320
 * Receives a string and tries to figure it out if it contains info to load a file.
5321
 * Checks for a | (pipe) symbol and tries to load a file with the info given.
5322
 * 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.
5323
 *
5324
 * @param string $string The string containing a valid format.
5325
 * @return string|boolean The given string with the pipe and file info removed. Boolean false if the file couldn't be loaded.
5326
 */
5327
function load_file($string)
5328
{
5329
	global $sourcedir, $txt, $boarddir, $settings, $context;
5330
5331
	if (empty($string))
5332
		return false;
5333
5334
	if (strpos($string, '|') !== false)
5335
	{
5336
		list ($file, $string) = explode('|', $string);
5337
5338
		// Match the wildcards to their regular vars.
5339
		if (empty($settings['theme_dir']))
5340
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
5341
5342
		else
5343
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
5344
5345
		// Load the file if it can be loaded.
5346
		if (file_exists($absPath))
5347
			require_once($absPath);
5348
5349
		// No? try a fallback to $sourcedir
5350
		else
5351
		{
5352
			$absPath = $sourcedir . '/' . $file;
5353
5354
			if (file_exists($absPath))
5355
				require_once($absPath);
5356
5357
			// Sorry, can't do much for you at this point.
5358
			elseif (empty($context['uninstalling']))
5359
			{
5360
				loadLanguage('Errors');
5361
				log_error(sprintf($txt['hook_fail_loading_file'], $absPath), 'general');
5362
5363
				// File couldn't be loaded.
5364
				return false;
5365
			}
5366
		}
5367
	}
5368
5369
	return $string;
5370
}
5371
5372
/**
5373
 * Get the contents of a URL, irrespective of allow_url_fopen.
5374
 *
5375
 * - reads the contents of an http or ftp address and returns the page in a string
5376
 * - will accept up to 3 page redirections (redirectio_level in the function call is private)
5377
 * - if post_data is supplied, the value and length is posted to the given url as form data
5378
 * - URL must be supplied in lowercase
5379
 *
5380
 * @param string $url The URL
5381
 * @param string $post_data The data to post to the given URL
5382
 * @param bool $keep_alive Whether to send keepalive info
5383
 * @param int $redirection_level How many levels of redirection
5384
 * @return string|false The fetched data or false on failure
5385
 */
5386
function fetch_web_data($url, $post_data = '', $keep_alive = false, $redirection_level = 0)
5387
{
5388
	global $webmaster_email, $sourcedir;
5389
	static $keep_alive_dom = null, $keep_alive_fp = null;
5390
5391
	preg_match('~^(http|ftp)(s)?://([^/:]+)(:(\d+))?(.+)$~', $url, $match);
5392
5393
	// No scheme? No data for you!
5394
	if (empty($match[1]))
5395
		return false;
5396
5397
	// An FTP url. We should try connecting and RETRieving it...
5398
	elseif ($match[1] == 'ftp')
5399
	{
5400
		// Include the file containing the ftp_connection class.
5401
		require_once($sourcedir . '/Class-Package.php');
5402
5403
		// Establish a connection and attempt to enable passive mode.
5404
		$ftp = new ftp_connection(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? 21 : $match[5], 'anonymous', $webmaster_email);
5405
		if ($ftp->error !== false || !$ftp->passive())
0 ignored issues
show
introduced by
The condition $ftp->error !== false is always true.
Loading history...
5406
			return false;
5407
5408
		// I want that one *points*!
5409
		fwrite($ftp->connection, 'RETR ' . $match[6] . "\r\n");
5410
5411
		// Since passive mode worked (or we would have returned already!) open the connection.
5412
		$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...
5413
		if (!$fp)
5414
			return false;
5415
5416
		// The server should now say something in acknowledgement.
5417
		$ftp->check_response(150);
5418
5419
		$data = '';
5420
		while (!feof($fp))
5421
			$data .= fread($fp, 4096);
5422
		fclose($fp);
5423
5424
		// All done, right?  Good.
5425
		$ftp->check_response(226);
5426
		$ftp->close();
5427
	}
5428
5429
	// This is more likely; a standard HTTP URL.
5430
	elseif (isset($match[1]) && $match[1] == 'http')
5431
	{
5432
		// First try to use fsockopen, because it is fastest.
5433
		if ($keep_alive && $match[3] == $keep_alive_dom)
5434
			$fp = $keep_alive_fp;
5435
		if (empty($fp))
5436
		{
5437
			// Open the socket on the port we want...
5438
			$fp = @fsockopen(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? ($match[2] ? 443 : 80) : $match[5], $err, $err, 5);
5439
		}
5440
		if (!empty($fp))
5441
		{
5442
			if ($keep_alive)
5443
			{
5444
				$keep_alive_dom = $match[3];
5445
				$keep_alive_fp = $fp;
5446
			}
5447
5448
			// I want this, from there, and I'm not going to be bothering you for more (probably.)
5449
			if (empty($post_data))
5450
			{
5451
				fwrite($fp, 'GET ' . ($match[6] !== '/' ? str_replace(' ', '%20', $match[6]) : '') . ' HTTP/1.0' . "\r\n");
5452
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
5453
				fwrite($fp, 'user-agent: '. SMF_USER_AGENT . "\r\n");
5454
				if ($keep_alive)
5455
					fwrite($fp, 'connection: Keep-Alive' . "\r\n\r\n");
5456
				else
5457
					fwrite($fp, 'connection: close' . "\r\n\r\n");
5458
			}
5459
			else
5460
			{
5461
				fwrite($fp, 'POST ' . ($match[6] !== '/' ? $match[6] : '') . ' HTTP/1.0' . "\r\n");
5462
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
5463
				fwrite($fp, 'user-agent: '. SMF_USER_AGENT . "\r\n");
5464
				if ($keep_alive)
5465
					fwrite($fp, 'connection: Keep-Alive' . "\r\n");
5466
				else
5467
					fwrite($fp, 'connection: close' . "\r\n");
5468
				fwrite($fp, 'content-type: application/x-www-form-urlencoded' . "\r\n");
5469
				fwrite($fp, 'content-length: ' . strlen($post_data) . "\r\n\r\n");
5470
				fwrite($fp, $post_data);
5471
			}
5472
5473
			$response = fgets($fp, 768);
5474
5475
			// Redirect in case this location is permanently or temporarily moved.
5476
			if ($redirection_level < 3 && preg_match('~^HTTP/\S+\s+30[127]~i', $response) === 1)
5477
			{
5478
				$header = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $header is dead and can be removed.
Loading history...
5479
				$location = '';
5480
				while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
5481
					if (stripos($header, 'location:') !== false)
5482
						$location = trim(substr($header, strpos($header, ':') + 1));
5483
5484
				if (empty($location))
5485
					return false;
5486
				else
5487
				{
5488
					if (!$keep_alive)
5489
						fclose($fp);
5490
					return fetch_web_data($location, $post_data, $keep_alive, $redirection_level + 1);
5491
				}
5492
			}
5493
5494
			// Make sure we get a 200 OK.
5495
			elseif (preg_match('~^HTTP/\S+\s+20[01]~i', $response) === 0)
5496
				return false;
5497
5498
			// Skip the headers...
5499
			while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
5500
			{
5501
				if (preg_match('~content-length:\s*(\d+)~i', $header, $match) != 0)
5502
					$content_length = $match[1];
5503
				elseif (preg_match('~connection:\s*close~i', $header) != 0)
5504
				{
5505
					$keep_alive_dom = null;
5506
					$keep_alive = false;
5507
				}
5508
5509
				continue;
5510
			}
5511
5512
			$data = '';
5513
			if (isset($content_length))
5514
			{
5515
				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...
5516
					$data .= fread($fp, $content_length - strlen($data));
5517
			}
5518
			else
5519
			{
5520
				while (!feof($fp))
5521
					$data .= fread($fp, 4096);
5522
			}
5523
5524
			if (!$keep_alive)
5525
				fclose($fp);
5526
		}
5527
5528
		// If using fsockopen didn't work, try to use cURL if available.
5529
		elseif (function_exists('curl_init'))
5530
		{
5531
			// Include the file containing the curl_fetch_web_data class.
5532
			require_once($sourcedir . '/Class-CurlFetchWeb.php');
5533
5534
			$fetch_data = new curl_fetch_web_data();
5535
			$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

5535
			$fetch_data->get_url_data($url, /** @scrutinizer ignore-type */ $post_data);
Loading history...
5536
5537
			// no errors and a 200 result, then we have a good dataset, well we at least have data. ;)
5538
			if ($fetch_data->result('code') == 200 && !$fetch_data->result('error'))
5539
				$data = $fetch_data->result('body');
5540
			else
5541
				return false;
5542
		}
5543
5544
		// Neither fsockopen nor curl are available. Well, phooey.
5545
		else
5546
			return false;
5547
	}
5548
	else
5549
	{
5550
		// Umm, this shouldn't happen?
5551
		trigger_error('fetch_web_data(): Bad URL', E_USER_NOTICE);
5552
		$data = false;
5553
	}
5554
5555
	return $data;
5556
}
5557
5558
/**
5559
 * Attempts to determine the MIME type of some data or a file.
5560
 *
5561
 * @param string $data The data to check, or the path or URL of a file to check.
5562
 * @param string $is_path If true, $data is a path or URL to a file.
5563
 * @return string|bool A MIME type, or false if we cannot determine it.
5564
 */
5565
function get_mime_type($data, $is_path = false)
5566
{
5567
	global $cachedir;
5568
5569
	$finfo_loaded = extension_loaded('fileinfo');
5570
	$exif_loaded = extension_loaded('exif') && function_exists('image_type_to_mime_type');
5571
5572
	// Oh well. We tried.
5573
	if (!$finfo_loaded && !$exif_loaded)
5574
		return false;
5575
5576
	// Start with the 'empty' MIME type.
5577
	$mime_type = 'application/x-empty';
5578
5579
	if ($finfo_loaded)
5580
	{
5581
		// Just some nice, simple data to analyze.
5582
		if (empty($is_path))
5583
			$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
5584
5585
		// A file, or maybe a URL?
5586
		else
5587
		{
5588
			// Local file.
5589
			if (file_exists($data))
5590
				$mime_type = mime_content_type($data);
5591
5592
			// URL.
5593
			elseif ($data = fetch_web_data($data))
5594
				$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
5595
		}
5596
	}
5597
	// Workaround using Exif requires a local file.
5598
	else
5599
	{
5600
		// If $data is a URL to fetch, do so.
5601
		if (!empty($is_path) && !file_exists($data) && url_exists($data))
5602
		{
5603
			$data = fetch_web_data($data);
5604
			$is_path = false;
5605
		}
5606
5607
		// If we don't have a local file, create one and use it.
5608
		if (empty($is_path))
5609
		{
5610
			$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 $str 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

5610
			$temp_file = tempnam($cachedir, md5(/** @scrutinizer ignore-type */ $data));
Loading history...
5611
			file_put_contents($temp_file, $data);
5612
			$is_path = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $is_path is dead and can be removed.
Loading history...
5613
			$data = $temp_file;
5614
		}
5615
5616
		$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

5616
		$imagetype = @exif_imagetype(/** @scrutinizer ignore-type */ $data);
Loading history...
5617
5618
		if (isset($temp_file))
5619
			unlink($temp_file);
5620
5621
		// Unfortunately, this workaround only works for image files.
5622
		if ($imagetype !== false)
5623
			$mime_type = image_type_to_mime_type($imagetype);
5624
	}
5625
5626
	return $mime_type;
5627
}
5628
5629
/**
5630
 * Checks whether a file or data has the expected MIME type.
5631
 *
5632
 * @param string $data The data to check, or the path or URL of a file to check.
5633
 * @param string $type_pattern A regex pattern to match the acceptable MIME types.
5634
 * @param string $is_path If true, $data is a path or URL to a file.
5635
 * @return int 1 if the detected MIME type matches the pattern, 0 if it doesn't, or 2 if we can't check.
5636
 */
5637
function check_mime_type($data, $type_pattern, $is_path = false)
5638
{
5639
	// Get the MIME type.
5640
	$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

5640
	$mime_type = get_mime_type($data, /** @scrutinizer ignore-type */ $is_path);
Loading history...
5641
5642
	// Couldn't determine it.
5643
	if ($mime_type === false)
5644
		return 2;
5645
5646
	// Check whether the MIME type matches expectations.
5647
	return (int) @preg_match('~' . $type_pattern . '~', $mime_type);
5648
}
5649
5650
/**
5651
 * Prepares an array of "likes" info for the topic specified by $topic
5652
 *
5653
 * @param integer $topic The topic ID to fetch the info from.
5654
 * @return array An array of IDs of messages in the specified topic that the current user likes
5655
 */
5656
function prepareLikesContext($topic)
5657
{
5658
	global $user_info, $smcFunc;
5659
5660
	// Make sure we have something to work with.
5661
	if (empty($topic))
5662
		return array();
5663
5664
	// We already know the number of likes per message, we just want to know whether the current user liked it or not.
5665
	$user = $user_info['id'];
5666
	$cache_key = 'likes_topic_' . $topic . '_' . $user;
5667
	$ttl = 180;
5668
5669
	if (($temp = cache_get_data($cache_key, $ttl)) === null)
5670
	{
5671
		$temp = array();
5672
		$request = $smcFunc['db_query']('', '
5673
			SELECT content_id
5674
			FROM {db_prefix}user_likes AS l
5675
				INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg)
5676
			WHERE l.id_member = {int:current_user}
5677
				AND l.content_type = {literal:msg}
5678
				AND m.id_topic = {int:topic}',
5679
			array(
5680
				'current_user' => $user,
5681
				'topic' => $topic,
5682
			)
5683
		);
5684
		while ($row = $smcFunc['db_fetch_assoc']($request))
5685
			$temp[] = (int) $row['content_id'];
5686
5687
		cache_put_data($cache_key, $temp, $ttl);
5688
	}
5689
5690
	return $temp;
5691
}
5692
5693
/**
5694
 * Microsoft uses their own character set Code Page 1252 (CP1252), which is a
5695
 * superset of ISO 8859-1, defining several characters between DEC 128 and 159
5696
 * that are not normally displayable.  This converts the popular ones that
5697
 * appear from a cut and paste from windows.
5698
 *
5699
 * @param string $string The string
5700
 * @return string The sanitized string
5701
 */
5702
function sanitizeMSCutPaste($string)
5703
{
5704
	global $context;
5705
5706
	if (empty($string))
5707
		return $string;
5708
5709
	// UTF-8 occurences of MS special characters
5710
	$findchars_utf8 = array(
5711
		"\xe2\x80\x9a",	// single low-9 quotation mark
5712
		"\xe2\x80\x9e",	// double low-9 quotation mark
5713
		"\xe2\x80\xa6",	// horizontal ellipsis
5714
		"\xe2\x80\x98",	// left single curly quote
5715
		"\xe2\x80\x99",	// right single curly quote
5716
		"\xe2\x80\x9c",	// left double curly quote
5717
		"\xe2\x80\x9d",	// right double curly quote
5718
	);
5719
5720
	// windows 1252 / iso equivalents
5721
	$findchars_iso = array(
5722
		chr(130),
5723
		chr(132),
5724
		chr(133),
5725
		chr(145),
5726
		chr(146),
5727
		chr(147),
5728
		chr(148),
5729
	);
5730
5731
	// safe replacements
5732
	$replacechars = array(
5733
		',',	// &sbquo;
5734
		',,',	// &bdquo;
5735
		'...',	// &hellip;
5736
		"'",	// &lsquo;
5737
		"'",	// &rsquo;
5738
		'"',	// &ldquo;
5739
		'"',	// &rdquo;
5740
	);
5741
5742
	if ($context['utf8'])
5743
		$string = str_replace($findchars_utf8, $replacechars, $string);
5744
	else
5745
		$string = str_replace($findchars_iso, $replacechars, $string);
5746
5747
	return $string;
5748
}
5749
5750
/**
5751
 * Decode numeric html entities to their ascii or UTF8 equivalent character.
5752
 *
5753
 * Callback function for preg_replace_callback in subs-members
5754
 * Uses capture group 2 in the supplied array
5755
 * Does basic scan to ensure characters are inside a valid range
5756
 *
5757
 * @param array $matches An array of matches (relevant info should be the 3rd item)
5758
 * @return string A fixed string
5759
 */
5760
function replaceEntities__callback($matches)
5761
{
5762
	global $context;
5763
5764
	if (!isset($matches[2]))
5765
		return '';
5766
5767
	$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

5767
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5768
5769
	// remove left to right / right to left overrides
5770
	if ($num === 0x202D || $num === 0x202E)
5771
		return '';
5772
5773
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
5774
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
5775
		return '&#' . $num . ';';
5776
5777
	if (empty($context['utf8']))
5778
	{
5779
		// no control characters
5780
		if ($num < 0x20)
5781
			return '';
5782
		// text is text
5783
		elseif ($num < 0x80)
5784
			return chr($num);
0 ignored issues
show
Bug introduced by
It seems like $num can also be of type double; however, parameter $ascii of chr() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

5784
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5785
		// all others get html-ised
5786
		else
5787
			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

5787
			return '&#' . /** @scrutinizer ignore-type */ $matches[2] . ';';
Loading history...
5788
	}
5789
	else
5790
	{
5791
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
5792
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
5793
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
5794
			return '';
5795
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
5796
		elseif ($num < 0x80)
5797
			return chr($num);
5798
		// <0x800 (2048)
5799
		elseif ($num < 0x800)
5800
			return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5801
		// < 0x10000 (65536)
5802
		elseif ($num < 0x10000)
5803
			return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5804
		// <= 0x10FFFF (1114111)
5805
		else
5806
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5807
	}
5808
}
5809
5810
/**
5811
 * Converts html entities to utf8 equivalents
5812
 *
5813
 * Callback function for preg_replace_callback
5814
 * Uses capture group 1 in the supplied array
5815
 * Does basic checks to keep characters inside a viewable range.
5816
 *
5817
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
5818
 * @return string The fixed string
5819
 */
5820
function fixchar__callback($matches)
5821
{
5822
	if (!isset($matches[1]))
5823
		return '';
5824
5825
	$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

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

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

5833
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5834
	// <0x800 (2048)
5835
	elseif ($num < 0x800)
5836
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5837
	// < 0x10000 (65536)
5838
	elseif ($num < 0x10000)
5839
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5840
	// <= 0x10FFFF (1114111)
5841
	else
5842
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5843
}
5844
5845
/**
5846
 * Strips out invalid html entities, replaces others with html style &#123; codes
5847
 *
5848
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
5849
 * strpos, strlen, substr etc
5850
 *
5851
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
5852
 * @return string The fixed string
5853
 */
5854
function entity_fix__callback($matches)
5855
{
5856
	if (!isset($matches[2]))
5857
		return '';
5858
5859
	$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

5859
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5860
5861
	// we don't allow control characters, characters out of range, byte markers, etc
5862
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
5863
		return '';
5864
	else
5865
		return '&#' . $num . ';';
5866
}
5867
5868
/**
5869
 * Return a Gravatar URL based on
5870
 * - the supplied email address,
5871
 * - the global maximum rating,
5872
 * - the global default fallback,
5873
 * - maximum sizes as set in the admin panel.
5874
 *
5875
 * It is SSL aware, and caches most of the parameters.
5876
 *
5877
 * @param string $email_address The user's email address
5878
 * @return string The gravatar URL
5879
 */
5880
function get_gravatar_url($email_address)
5881
{
5882
	global $modSettings, $smcFunc;
5883
	static $url_params = null;
5884
5885
	if ($url_params === null)
5886
	{
5887
		$ratings = array('G', 'PG', 'R', 'X');
5888
		$defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank');
5889
		$url_params = array();
5890
		if (!empty($modSettings['gravatarMaxRating']) && in_array($modSettings['gravatarMaxRating'], $ratings))
5891
			$url_params[] = 'rating=' . $modSettings['gravatarMaxRating'];
5892
		if (!empty($modSettings['gravatarDefault']) && in_array($modSettings['gravatarDefault'], $defaults))
5893
			$url_params[] = 'default=' . $modSettings['gravatarDefault'];
5894
		if (!empty($modSettings['avatar_max_width_external']))
5895
			$size_string = (int) $modSettings['avatar_max_width_external'];
5896
		if (!empty($modSettings['avatar_max_height_external']) && !empty($size_string))
5897
			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...
5898
				$size_string = $modSettings['avatar_max_height_external'];
5899
5900
		if (!empty($size_string))
5901
			$url_params[] = 's=' . $size_string;
5902
	}
5903
	$http_method = !empty($modSettings['force_ssl']) ? 'https://secure' : 'http://www';
5904
5905
	return $http_method . '.gravatar.com/avatar/' . md5($smcFunc['strtolower']($email_address)) . '?' . implode('&', $url_params);
5906
}
5907
5908
/**
5909
 * Get a list of timezones.
5910
 *
5911
 * @param string $when An optional date or time for which to calculate the timezone offset values. May be a Unix timestamp or any string that strtotime() can understand. Defaults to 'now'.
5912
 * @return array An array of timezone info.
5913
 */
5914
function smf_list_timezones($when = 'now')
5915
{
5916
	global $smcFunc, $modSettings, $tztxt, $txt, $cur_profile;
5917
	static $timezones = null, $lastwhen = null;
5918
5919
	// No point doing this over if we already did it once
5920
	if (!empty($timezones) && $when == $lastwhen)
5921
		return $timezones;
5922
	else
5923
		$lastwhen = $when;
5924
5925
	// Parseable datetime string?
5926
	if (is_int($timestamp = strtotime($when)))
0 ignored issues
show
introduced by
The condition is_int($timestamp = strtotime($when)) is always true.
Loading history...
5927
		$when = $timestamp;
5928
5929
	// A Unix timestamp?
5930
	elseif (is_numeric($when))
5931
		$when = intval($when);
5932
5933
	// Invalid value? Just get current Unix timestamp.
5934
	else
5935
		$when = time();
5936
5937
	// We'll need these too
5938
	$date_when = date_create('@' . $when);
5939
	$later = strtotime('@' . $when . ' + 1 year');
5940
5941
	// Load up any custom time zone descriptions we might have
5942
	loadLanguage('Timezones');
5943
5944
	// Should we put time zones from certain countries at the top of the list?
5945
	$priority_countries = !empty($modSettings['timezone_priority_countries']) ? explode(',', $modSettings['timezone_priority_countries']) : array();
5946
	$priority_tzids = array();
5947
	foreach ($priority_countries as $country)
5948
	{
5949
		$country_tzids = @timezone_identifiers_list(DateTimeZone::PER_COUNTRY, strtoupper(trim($country)));
5950
		if (!empty($country_tzids))
5951
			$priority_tzids = array_merge($priority_tzids, $country_tzids);
5952
	}
5953
5954
	// Antarctic research stations should be listed last, unless you're running a penguin forum
5955
	$low_priority_tzids = !in_array('AQ', $priority_countries) ? timezone_identifiers_list(DateTimeZone::ANTARCTICA) : array();
5956
5957
	// Process the preferred timezones first, then the normal ones, then the low priority ones.
5958
	$tzids = array_merge(array_keys($tztxt), array_diff(timezone_identifiers_list(), array_keys($tztxt), $low_priority_tzids), $low_priority_tzids);
0 ignored issues
show
Bug introduced by
It seems like $low_priority_tzids can also be of type false; however, parameter $_ of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

5958
	$tzids = array_merge(array_keys($tztxt), array_diff(timezone_identifiers_list(), array_keys($tztxt), /** @scrutinizer ignore-type */ $low_priority_tzids), $low_priority_tzids);
Loading history...
5959
5960
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
5961
	foreach ($tzids as $tzid)
5962
	{
5963
		// We don't want UTC right now
5964
		if ($tzid == 'UTC')
5965
			continue;
5966
5967
		$tz = timezone_open($tzid);
5968
5969
		// First, get the set of transition rules for this tzid
5970
		$tzinfo = timezone_transitions_get($tz, $when, $later);
0 ignored issues
show
Bug introduced by
It seems like $tz can also be of type false; however, parameter $object of timezone_transitions_get() does only seem to accept DateTimeZone, maybe add an additional type check? ( Ignorable by Annotation )

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

5970
		$tzinfo = timezone_transitions_get(/** @scrutinizer ignore-type */ $tz, $when, $later);
Loading history...
5971
5972
		// Use the entire set of transition rules as the array *key* so we can avoid duplicates
5973
		$tzkey = serialize($tzinfo);
5974
5975
		// Next, get the geographic info for this tzid
5976
		$tzgeo = timezone_location_get($tz);
0 ignored issues
show
Bug introduced by
It seems like $tz can also be of type false; however, parameter $object of timezone_location_get() does only seem to accept DateTimeZone, maybe add an additional type check? ( Ignorable by Annotation )

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

5976
		$tzgeo = timezone_location_get(/** @scrutinizer ignore-type */ $tz);
Loading history...
5977
5978
		// Don't overwrite our preferred tzids
5979
		if (empty($zones[$tzkey]['tzid']))
5980
		{
5981
			$zones[$tzkey]['tzid'] = $tzid;
5982
			$zones[$tzkey]['abbr'] = $tzinfo[0]['abbr'];
5983
		}
5984
5985
		// A time zone from a prioritized country?
5986
		if (in_array($tzid, $priority_tzids))
5987
			$priority_zones[$tzkey] = true;
5988
5989
		// Keep track of the location and offset for this tzid
5990
		if (!empty($txt[$tzid]))
5991
			$zones[$tzkey]['locations'][] = $txt[$tzid];
5992
		else
5993
		{
5994
			$tzid_parts = explode('/', $tzid);
5995
			$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
5996
		}
5997
		$offsets[$tzkey] = $tzinfo[0]['offset'];
5998
		$longitudes[$tzkey] = empty($longitudes[$tzkey]) ? $tzgeo['longitude'] : $longitudes[$tzkey];
5999
6000
		// Remember this for later
6001
		if (isset($cur_profile['timezone']) && $cur_profile['timezone'] == $tzid)
6002
			$member_tzkey = $tzkey;
6003
	}
6004
6005
	// Sort by offset then longitude
6006
	array_multisort($offsets, SORT_ASC, SORT_NUMERIC, $longitudes, SORT_ASC, SORT_NUMERIC, $zones);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $longitudes seems to be defined by a foreach iteration on line 5961. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
Comprehensibility Best Practice introduced by
The variable $zones seems to be defined by a foreach iteration on line 5961. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
6007
6008
	// Build the final array of formatted values
6009
	$priority_timezones = array();
6010
	$timezones = array();
6011
	foreach ($zones as $tzkey => $tzvalue)
6012
	{
6013
		date_timezone_set($date_when, timezone_open($tzvalue['tzid']));
0 ignored issues
show
Bug introduced by
It seems like $date_when can also be of type false; however, parameter $object of date_timezone_set() does only seem to accept DateTime, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

6013
		date_timezone_set($date_when, /** @scrutinizer ignore-type */ timezone_open($tzvalue['tzid']));
Loading history...
6014
6015
		// Use the custom description, if there is one
6016
		if (!empty($tztxt[$tzvalue['tzid']]))
6017
			$desc = $tztxt[$tzvalue['tzid']];
6018
		// Otherwise, use the list of locations (max 5, so things don't get silly)
6019
		else
6020
			$desc = implode(', ', array_slice(array_unique($tzvalue['locations']), 0, 5)) . (count($tzvalue['locations']) > 5 ? ', ' . $txt['etc'] : '');
6021
6022
		// Show the UTC offset and the abbreviation, if it's something like 'MST' and not '-06'
6023
		$desc = '[UTC' . date_format($date_when, 'P') . '] - ' . (!strspn($tzvalue['abbr'], '+-') ? $tzvalue['abbr'] . ' - ' : '') . $desc;
6024
6025
		if (isset($priority_zones[$tzkey]))
6026
			$priority_timezones[$tzvalue['tzid']] = $desc;
6027
		else
6028
			$timezones[$tzvalue['tzid']] = $desc;
6029
6030
		// Automatically fix orphaned timezones on the member profile page
6031
		if (isset($member_tzkey) && $member_tzkey == $tzkey)
6032
			$cur_profile['timezone'] = $tzvalue['tzid'];
6033
	}
6034
6035
	if (!empty($priority_timezones))
6036
		$priority_timezones[] = '-----';
6037
6038
	$timezones = array_merge(
6039
		$priority_timezones,
6040
		array('UTC' => 'UTC' . (!empty($tztxt['UTC']) ? ' - ' . $tztxt['UTC'] : ''), '-----'),
6041
		$timezones
6042
	);
6043
6044
	return $timezones;
6045
}
6046
6047
/**
6048
 * Converts an IP address into binary
6049
 *
6050
 * @param string $ip_address An IP address in IPv4, IPv6 or decimal notation
6051
 * @return string|false The IP address in binary or false
6052
 */
6053
function inet_ptod($ip_address)
6054
{
6055
	if (!isValidIP($ip_address))
6056
		return $ip_address;
6057
6058
	$bin = inet_pton($ip_address);
6059
	return $bin;
6060
}
6061
6062
/**
6063
 * Converts a binary version of an IP address into a readable format
6064
 *
6065
 * @param string $bin An IP address in IPv4, IPv6 (Either string (postgresql) or binary (other databases))
6066
 * @return string|false The IP address in presentation format or false on error
6067
 */
6068
function inet_dtop($bin)
6069
{
6070
	global $db_type;
6071
6072
	if (empty($bin))
6073
		return '';
6074
	elseif ($db_type == 'postgresql')
6075
		return $bin;
6076
	// Already a String?
6077
	elseif (isValidIP($bin))
6078
		return $bin;
6079
	return inet_ntop($bin);
6080
}
6081
6082
/**
6083
 * Safe serialize() and unserialize() replacements
6084
 *
6085
 * @license Public Domain
6086
 *
6087
 * @author anthon (dot) pang (at) gmail (dot) com
6088
 */
6089
6090
/**
6091
 * Safe serialize() replacement. Recursive
6092
 * - output a strict subset of PHP's native serialized representation
6093
 * - does not serialize objects
6094
 *
6095
 * @param mixed $value
6096
 * @return string
6097
 */
6098
function _safe_serialize($value)
6099
{
6100
	if (is_null($value))
6101
		return 'N;';
6102
6103
	if (is_bool($value))
6104
		return 'b:' . (int) $value . ';';
6105
6106
	if (is_int($value))
6107
		return 'i:' . $value . ';';
6108
6109
	if (is_float($value))
6110
		return 'd:' . str_replace(',', '.', $value) . ';';
6111
6112
	if (is_string($value))
6113
		return 's:' . strlen($value) . ':"' . $value . '";';
6114
6115
	if (is_array($value))
6116
	{
6117
		$out = '';
6118
		foreach ($value as $k => $v)
6119
			$out .= _safe_serialize($k) . _safe_serialize($v);
6120
6121
		return 'a:' . count($value) . ':{' . $out . '}';
6122
	}
6123
6124
	// safe_serialize cannot serialize resources or objects.
6125
	return false;
6126
}
6127
6128
/**
6129
 * Wrapper for _safe_serialize() that handles exceptions and multibyte encoding issues.
6130
 *
6131
 * @param mixed $value
6132
 * @return string
6133
 */
6134
function safe_serialize($value)
6135
{
6136
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
6137
	if (function_exists('mb_internal_encoding') &&
6138
		(((int) ini_get('mbstring.func_overload')) & 2))
6139
	{
6140
		$mbIntEnc = mb_internal_encoding();
6141
		mb_internal_encoding('ASCII');
6142
	}
6143
6144
	$out = _safe_serialize($value);
6145
6146
	if (isset($mbIntEnc))
6147
		mb_internal_encoding($mbIntEnc);
6148
6149
	return $out;
6150
}
6151
6152
/**
6153
 * Safe unserialize() replacement
6154
 * - accepts a strict subset of PHP's native serialized representation
6155
 * - does not unserialize objects
6156
 *
6157
 * @param string $str
6158
 * @return mixed
6159
 * @throw Exception if $str is malformed or contains unsupported types (e.g., resources, objects)
6160
 */
6161
function _safe_unserialize($str)
6162
{
6163
	// Input  is not a string.
6164
	if (empty($str) || !is_string($str))
6165
		return false;
6166
6167
	$stack = array();
6168
	$expected = array();
6169
6170
	/*
6171
	 * states:
6172
	 *   0 - initial state, expecting a single value or array
6173
	 *   1 - terminal state
6174
	 *   2 - in array, expecting end of array or a key
6175
	 *   3 - in array, expecting value or another array
6176
	 */
6177
	$state = 0;
6178
	while ($state != 1)
6179
	{
6180
		$type = isset($str[0]) ? $str[0] : '';
6181
		if ($type == '}')
6182
			$str = substr($str, 1);
6183
6184
		elseif ($type == 'N' && $str[1] == ';')
6185
		{
6186
			$value = null;
6187
			$str = substr($str, 2);
6188
		}
6189
		elseif ($type == 'b' && preg_match('/^b:([01]);/', $str, $matches))
6190
		{
6191
			$value = $matches[1] == '1' ? true : false;
6192
			$str = substr($str, 4);
6193
		}
6194
		elseif ($type == 'i' && preg_match('/^i:(-?[0-9]+);(.*)/s', $str, $matches))
6195
		{
6196
			$value = (int) $matches[1];
6197
			$str = $matches[2];
6198
		}
6199
		elseif ($type == 'd' && preg_match('/^d:(-?[0-9]+\.?[0-9]*(E[+-][0-9]+)?);(.*)/s', $str, $matches))
6200
		{
6201
			$value = (float) $matches[1];
6202
			$str = $matches[3];
6203
		}
6204
		elseif ($type == 's' && preg_match('/^s:([0-9]+):"(.*)/s', $str, $matches) && substr($matches[2], (int) $matches[1], 2) == '";')
6205
		{
6206
			$value = substr($matches[2], 0, (int) $matches[1]);
6207
			$str = substr($matches[2], (int) $matches[1] + 2);
6208
		}
6209
		elseif ($type == 'a' && preg_match('/^a:([0-9]+):{(.*)/s', $str, $matches))
6210
		{
6211
			$expectedLength = (int) $matches[1];
6212
			$str = $matches[2];
6213
		}
6214
6215
		// Object or unknown/malformed type.
6216
		else
6217
			return false;
6218
6219
		switch ($state)
6220
		{
6221
			case 3: // In array, expecting value or another array.
6222
				if ($type == 'a')
6223
				{
6224
					$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...
6225
					$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...
6226
					$list = &$list[$key];
6227
					$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...
6228
					$state = 2;
6229
					break;
6230
				}
6231
				if ($type != '}')
6232
				{
6233
					$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...
6234
					$state = 2;
6235
					break;
6236
				}
6237
6238
				// Missing array value.
6239
				return false;
6240
6241
			case 2: // in array, expecting end of array or a key
6242
				if ($type == '}')
6243
				{
6244
					// Array size is less than expected.
6245
					if (count($list) < end($expected))
6246
						return false;
6247
6248
					unset($list);
6249
					$list = &$stack[count($stack) - 1];
6250
					array_pop($stack);
6251
6252
					// Go to terminal state if we're at the end of the root array.
6253
					array_pop($expected);
6254
6255
					if (count($expected) == 0)
6256
						$state = 1;
6257
6258
					break;
6259
				}
6260
6261
				if ($type == 'i' || $type == 's')
6262
				{
6263
					// Array size exceeds expected length.
6264
					if (count($list) >= end($expected))
6265
						return false;
6266
6267
					$key = $value;
6268
					$state = 3;
6269
					break;
6270
				}
6271
6272
				// Illegal array index type.
6273
				return false;
6274
6275
			// Expecting array or value.
6276
			case 0:
6277
				if ($type == 'a')
6278
				{
6279
					$data = array();
6280
					$list = &$data;
6281
					$expected[] = $expectedLength;
6282
					$state = 2;
6283
					break;
6284
				}
6285
6286
				if ($type != '}')
6287
				{
6288
					$data = $value;
6289
					$state = 1;
6290
					break;
6291
				}
6292
6293
				// Not in array.
6294
				return false;
6295
		}
6296
	}
6297
6298
	// Trailing data in input.
6299
	if (!empty($str))
6300
		return false;
6301
6302
	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...
6303
}
6304
6305
/**
6306
 * Wrapper for _safe_unserialize() that handles exceptions and multibyte encoding issue
6307
 *
6308
 * @param string $str
6309
 * @return mixed
6310
 */
6311
function safe_unserialize($str)
6312
{
6313
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
6314
	if (function_exists('mb_internal_encoding') &&
6315
		(((int) ini_get('mbstring.func_overload')) & 0x02))
6316
	{
6317
		$mbIntEnc = mb_internal_encoding();
6318
		mb_internal_encoding('ASCII');
6319
	}
6320
6321
	$out = _safe_unserialize($str);
6322
6323
	if (isset($mbIntEnc))
6324
		mb_internal_encoding($mbIntEnc);
6325
6326
	return $out;
6327
}
6328
6329
/**
6330
 * Tries different modes to make file/dirs writable. Wrapper function for chmod()
6331
 *
6332
 * @param string $file The file/dir full path.
6333
 * @param int $value Not needed, added for legacy reasons.
6334
 * @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.
6335
 */
6336
function smf_chmod($file, $value = 0)
6337
{
6338
	// No file? no checks!
6339
	if (empty($file))
6340
		return false;
6341
6342
	// Already writable?
6343
	if (is_writable($file))
6344
		return true;
6345
6346
	// Do we have a file or a dir?
6347
	$isDir = is_dir($file);
6348
	$isWritable = false;
6349
6350
	// Set different modes.
6351
	$chmodValues = $isDir ? array(0750, 0755, 0775, 0777) : array(0644, 0664, 0666);
6352
6353
	foreach ($chmodValues as $val)
6354
	{
6355
		// If it's writable, break out of the loop.
6356
		if (is_writable($file))
6357
		{
6358
			$isWritable = true;
6359
			break;
6360
		}
6361
6362
		else
6363
			@chmod($file, $val);
6364
	}
6365
6366
	return $isWritable;
6367
}
6368
6369
/**
6370
 * Wrapper function for json_decode() with error handling.
6371
 *
6372
 * @param string $json The string to decode.
6373
 * @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.
6374
 * @param bool $logIt To specify if the error will be logged if theres any.
6375
 * @return array Either an empty array or the decoded data as an array.
6376
 */
6377
function smf_json_decode($json, $returnAsArray = false, $logIt = true)
6378
{
6379
	global $txt;
6380
6381
	// Come on...
6382
	if (empty($json) || !is_string($json))
6383
		return array();
6384
6385
	$returnArray = @json_decode($json, $returnAsArray);
6386
6387
	// PHP 5.3 so no json_last_error_msg()
6388
	switch (json_last_error())
6389
	{
6390
		case JSON_ERROR_NONE:
6391
			$jsonError = false;
6392
			break;
6393
		case JSON_ERROR_DEPTH:
6394
			$jsonError = 'JSON_ERROR_DEPTH';
6395
			break;
6396
		case JSON_ERROR_STATE_MISMATCH:
6397
			$jsonError = 'JSON_ERROR_STATE_MISMATCH';
6398
			break;
6399
		case JSON_ERROR_CTRL_CHAR:
6400
			$jsonError = 'JSON_ERROR_CTRL_CHAR';
6401
			break;
6402
		case JSON_ERROR_SYNTAX:
6403
			$jsonError = 'JSON_ERROR_SYNTAX';
6404
			break;
6405
		case JSON_ERROR_UTF8:
6406
			$jsonError = 'JSON_ERROR_UTF8';
6407
			break;
6408
		default:
6409
			$jsonError = 'unknown';
6410
			break;
6411
	}
6412
6413
	// Something went wrong!
6414
	if (!empty($jsonError) && $logIt)
6415
	{
6416
		// Being a wrapper means we lost our smf_error_handler() privileges :(
6417
		$jsonDebug = debug_backtrace();
6418
		$jsonDebug = $jsonDebug[0];
6419
		loadLanguage('Errors');
6420
6421
		if (!empty($jsonDebug))
6422
			log_error($txt['json_' . $jsonError], 'critical', $jsonDebug['file'], $jsonDebug['line']);
6423
6424
		else
6425
			log_error($txt['json_' . $jsonError], 'critical');
6426
6427
		// Everyone expects an array.
6428
		return array();
6429
	}
6430
6431
	return $returnArray;
6432
}
6433
6434
/**
6435
 * Check the given String if he is a valid IPv4 or IPv6
6436
 * return true or false
6437
 *
6438
 * @param string $IPString
6439
 *
6440
 * @return bool
6441
 */
6442
function isValidIP($IPString)
6443
{
6444
	return filter_var($IPString, FILTER_VALIDATE_IP) !== false;
6445
}
6446
6447
/**
6448
 * Outputs a response.
6449
 * It assumes the data is already a string.
6450
 *
6451
 * @param string $data The data to print
6452
 * @param string $type The content type. Defaults to Json.
6453
 * @return void
6454
 */
6455
function smf_serverResponse($data = '', $type = 'content-type: application/json')
6456
{
6457
	global $db_show_debug, $modSettings;
6458
6459
	// Defensive programming anyone?
6460
	if (empty($data))
6461
		return false;
6462
6463
	// Don't need extra stuff...
6464
	$db_show_debug = false;
6465
6466
	// Kill anything else.
6467
	ob_end_clean();
6468
6469
	if (!empty($modSettings['CompressedOutput']))
6470
		@ob_start('ob_gzhandler');
6471
6472
	else
6473
		ob_start();
6474
6475
	// Set the header.
6476
	header($type);
6477
6478
	// Echo!
6479
	echo $data;
6480
6481
	// Done.
6482
	obExit(false);
6483
}
6484
6485
/**
6486
 * Creates an optimized regex to match all known top level domains.
6487
 *
6488
 * The optimized regex is stored in $modSettings['tld_regex'].
6489
 *
6490
 * To update the stored version of the regex to use the latest list of valid
6491
 * TLDs from iana.org, set the $update parameter to true. Updating can take some
6492
 * time, based on network connectivity, so it should normally only be done by
6493
 * calling this function from a background or scheduled task.
6494
 *
6495
 * If $update is not true, but the regex is missing or invalid, the regex will
6496
 * be regenerated from a hard-coded list of TLDs. This regenerated regex will be
6497
 * overwritten on the next scheduled update.
6498
 *
6499
 * @param bool $update If true, fetch and process the latest official list of TLDs from iana.org.
6500
 */
6501
function set_tld_regex($update = false)
6502
{
6503
	global $sourcedir, $smcFunc, $modSettings;
6504
	static $done = false;
6505
6506
	// If we don't need to do anything, don't
6507
	if (!$update && $done)
6508
		return;
6509
6510
	// Should we get a new copy of the official list of TLDs?
6511
	if ($update)
6512
	{
6513
		$tlds = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt');
6514
		$tlds_md5 = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt.md5');
6515
6516
		/**
6517
		 * If the Internet Assigned Numbers Authority can't be reached, the Internet is GONE!
6518
		 * We're probably running on a server hidden in a bunker deep underground to protect
6519
		 * it from marauding bandits roaming on the surface. We don't want to waste precious
6520
		 * electricity on pointlessly repeating background tasks, so we'll wait until the next
6521
		 * regularly scheduled update to see if civilization has been restored.
6522
		 */
6523
		if ($tlds === false || $tlds_md5 === false)
6524
			$postapocalypticNightmare = true;
6525
6526
		// Make sure nothing went horribly wrong along the way.
6527
		if (md5($tlds) != substr($tlds_md5, 0, 32))
0 ignored issues
show
Bug introduced by
It seems like $tlds can also be of type false; however, parameter $str 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

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

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

6527
		if (md5($tlds) != substr(/** @scrutinizer ignore-type */ $tlds_md5, 0, 32))
Loading history...
6528
			$tlds = array();
6529
	}
6530
	// If we aren't updating and the regex is valid, we're done
6531
	elseif (!empty($modSettings['tld_regex']) && @preg_match('~' . $modSettings['tld_regex'] . '~', null) !== false)
6532
	{
6533
		$done = true;
6534
		return;
6535
	}
6536
6537
	// If we successfully got an update, process the list into an array
6538
	if (!empty($tlds))
6539
	{
6540
		// Clean $tlds and convert it to an array
6541
		$tlds = array_filter(explode("\n", strtolower($tlds)), function($line)
6542
		{
6543
			$line = trim($line);
6544
			if (empty($line) || strlen($line) != strspn($line, 'abcdefghijklmnopqrstuvwxyz0123456789-'))
6545
				return false;
6546
			else
6547
				return true;
6548
		});
6549
6550
		// Convert Punycode to Unicode
6551
		require_once($sourcedir . '/Class-Punycode.php');
6552
		$Punycode = new Punycode();
6553
		$tlds = array_map(function($input) use ($Punycode)
6554
		{
6555
			return $Punycode->decode($input);
6556
		}, $tlds);
6557
	}
6558
	// Otherwise, use the 2012 list of gTLDs and ccTLDs for now and schedule a background update
6559
	else
6560
	{
6561
		$tlds = array('com', 'net', 'org', 'edu', 'gov', 'mil', 'aero', 'asia', 'biz',
6562
			'cat', 'coop', 'info', 'int', 'jobs', 'mobi', 'museum', 'name', 'post',
6563
			'pro', 'tel', 'travel', 'xxx', 'ac', 'ad', 'ae', 'af', 'ag', 'ai', 'al',
6564
			'am', 'ao', 'aq', 'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd',
6565
			'be', 'bf', 'bg', 'bh', 'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv',
6566
			'bw', 'by', 'bz', 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm',
6567
			'cn', 'co', 'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do',
6568
			'dz', 'ec', 'ee', 'eg', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo',
6569
			'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp',
6570
			'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu',
6571
			'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm', 'jo',
6572
			'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la',
6573
			'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md',
6574
			'me', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp', 'mq', 'mr', 'ms', 'mt',
6575
			'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf', 'ng', 'ni', 'nl',
6576
			'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl',
6577
			'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw',
6578
			'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn',
6579
			'so', 'sr', 'ss', 'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg',
6580
			'th', 'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua',
6581
			'ug', 'uk', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf',
6582
			'ws', 'ye', 'yt', 'za', 'zm', 'zw',
6583
		);
6584
6585
		// Schedule a background update, unless civilization has collapsed and/or we are having connectivity issues.
6586
		if (empty($postapocalypticNightmare))
6587
		{
6588
			$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
6589
				array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
6590
				array('$sourcedir/tasks/UpdateTldRegex.php', 'Update_TLD_Regex', '', 0), array()
6591
			);
6592
		}
6593
	}
6594
6595
	// Tack on some "special use domain names" that aren't in DNS but may possibly resolve.
6596
	// See https://www.iana.org/assignments/special-use-domain-names/ for more info.
6597
	$tlds = array_merge($tlds, array('local', 'onion', 'test'));
6598
6599
	// Get an optimized regex to match all the TLDs
6600
	$tld_regex = build_regex($tlds);
6601
6602
	// Remember the new regex in $modSettings
6603
	updateSettings(array('tld_regex' => $tld_regex));
6604
6605
	// Redundant repetition is redundant
6606
	$done = true;
6607
}
6608
6609
/**
6610
 * Creates optimized regular expressions from an array of strings.
6611
 *
6612
 * An optimized regex built using this function will be much faster than a
6613
 * simple regex built using `implode('|', $strings)` --- anywhere from several
6614
 * times to several orders of magnitude faster.
6615
 *
6616
 * However, the time required to build the optimized regex is approximately
6617
 * equal to the time it takes to execute the simple regex. Therefore, it is only
6618
 * worth calling this function if the resulting regex will be used more than
6619
 * once.
6620
 *
6621
 * Because PHP places an upper limit on the allowed length of a regex, very
6622
 * large arrays of $strings may not fit in a single regex. Normally, the excess
6623
 * strings will simply be dropped. However, if the $returnArray parameter is set
6624
 * to true, this function will build as many regexes as necessary to accommodate
6625
 * everything in $strings and return them in an array. You will need to iterate
6626
 * through all elements of the returned array in order to test all possible
6627
 * matches.
6628
 *
6629
 * @param array $strings An array of strings to make a regex for.
6630
 * @param string $delim An optional delimiter character to pass to preg_quote().
6631
 * @param bool $returnArray If true, returns an array of regexes.
6632
 * @return string|array One or more regular expressions to match any of the input strings.
6633
 */
6634
function build_regex($strings, $delim = null, $returnArray = false)
6635
{
6636
	global $smcFunc;
6637
	static $regexes = array();
6638
6639
	// If it's not an array, there's not much to do. ;)
6640
	if (!is_array($strings))
0 ignored issues
show
introduced by
The condition is_array($strings) is always true.
Loading history...
6641
		return preg_quote(@strval($strings), $delim);
6642
6643
	$regex_key = md5(json_encode(array($strings, $delim, $returnArray)));
6644
6645
	if (isset($regexes[$regex_key]))
6646
		return $regexes[$regex_key];
6647
6648
	// The mb_* functions are faster than the $smcFunc ones, but may not be available
6649
	if (function_exists('mb_internal_encoding') && function_exists('mb_detect_encoding') && function_exists('mb_strlen') && function_exists('mb_substr'))
6650
	{
6651
		if (($string_encoding = mb_detect_encoding(implode(' ', $strings))) !== false)
6652
		{
6653
			$current_encoding = mb_internal_encoding();
6654
			mb_internal_encoding($string_encoding);
6655
		}
6656
6657
		$strlen = 'mb_strlen';
6658
		$substr = 'mb_substr';
6659
	}
6660
	else
6661
	{
6662
		$strlen = $smcFunc['strlen'];
6663
		$substr = $smcFunc['substr'];
6664
	}
6665
6666
	// This recursive function creates the index array from the strings
6667
	$add_string_to_index = function($string, $index) use (&$strlen, &$substr, &$add_string_to_index)
6668
	{
6669
		static $depth = 0;
6670
		$depth++;
6671
6672
		$first = (string) @$substr($string, 0, 1);
6673
6674
		// No first character? That's no good.
6675
		if ($first === '')
6676
		{
6677
			// A nested array? Really? Ugh. Fine.
6678
			if (is_array($string) && $depth < 20)
6679
			{
6680
				foreach ($string as $str)
6681
					$index = $add_string_to_index($str, $index);
6682
			}
6683
6684
			$depth--;
6685
			return $index;
6686
		}
6687
6688
		if (empty($index[$first]))
6689
			$index[$first] = array();
6690
6691
		if ($strlen($string) > 1)
6692
		{
6693
			// Sanity check on recursion
6694
			if ($depth > 99)
6695
				$index[$first][$substr($string, 1)] = '';
6696
6697
			else
6698
				$index[$first] = $add_string_to_index($substr($string, 1), $index[$first]);
6699
		}
6700
		else
6701
			$index[$first][''] = '';
6702
6703
		$depth--;
6704
		return $index;
6705
	};
6706
6707
	// This recursive function turns the index array into a regular expression
6708
	$index_to_regex = function(&$index, $delim) use (&$strlen, &$index_to_regex)
6709
	{
6710
		static $depth = 0;
6711
		$depth++;
6712
6713
		// Absolute max length for a regex is 32768, but we might need wiggle room
6714
		$max_length = 30000;
6715
6716
		$regex = array();
6717
		$length = 0;
6718
6719
		foreach ($index as $key => $value)
6720
		{
6721
			$key_regex = preg_quote($key, $delim);
6722
			$new_key = $key;
6723
6724
			if (empty($value))
6725
				$sub_regex = '';
6726
			else
6727
			{
6728
				$sub_regex = $index_to_regex($value, $delim);
6729
6730
				if (count(array_keys($value)) == 1)
6731
				{
6732
					$new_key_array = explode('(?' . '>', $sub_regex);
6733
					$new_key .= $new_key_array[0];
6734
				}
6735
				else
6736
					$sub_regex = '(?' . '>' . $sub_regex . ')';
6737
			}
6738
6739
			if ($depth > 1)
6740
				$regex[$new_key] = $key_regex . $sub_regex;
6741
			else
6742
			{
6743
				if (($length += strlen($key_regex) + 1) < $max_length || empty($regex))
6744
				{
6745
					$regex[$new_key] = $key_regex . $sub_regex;
6746
					unset($index[$key]);
6747
				}
6748
				else
6749
					break;
6750
			}
6751
		}
6752
6753
		// Sort by key length and then alphabetically
6754
		uksort($regex, function($k1, $k2) use (&$strlen)
6755
		{
6756
			$l1 = $strlen($k1);
6757
			$l2 = $strlen($k2);
6758
6759
			if ($l1 == $l2)
6760
				return strcmp($k1, $k2) > 0 ? 1 : -1;
6761
			else
6762
				return $l1 > $l2 ? -1 : 1;
6763
		});
6764
6765
		$depth--;
6766
		return implode('|', $regex);
6767
	};
6768
6769
	// Now that the functions are defined, let's do this thing
6770
	$index = array();
6771
	$regex = '';
6772
6773
	foreach ($strings as $string)
6774
		$index = $add_string_to_index($string, $index);
6775
6776
	if ($returnArray === true)
6777
	{
6778
		$regex = array();
6779
		while (!empty($index))
6780
			$regex[] = '(?' . '>' . $index_to_regex($index, $delim) . ')';
6781
	}
6782
	else
6783
		$regex = '(?' . '>' . $index_to_regex($index, $delim) . ')';
6784
6785
	// Restore PHP's internal character encoding to whatever it was originally
6786
	if (!empty($current_encoding))
6787
		mb_internal_encoding($current_encoding);
6788
6789
	$regexes[$regex_key] = $regex;
6790
	return $regex;
6791
}
6792
6793
/**
6794
 * Check if the passed url has an SSL certificate.
6795
 *
6796
 * Returns true if a cert was found & false if not.
6797
 *
6798
 * @param string $url to check, in $boardurl format (no trailing slash).
6799
 */
6800
function ssl_cert_found($url)
6801
{
6802
	// This check won't work without OpenSSL
6803
	if (!extension_loaded('openssl'))
6804
		return true;
6805
6806
	// First, strip the subfolder from the passed url, if any
6807
	$parsedurl = parse_url($url);
6808
	$url = 'ssl://' . $parsedurl['host'] . ':443';
6809
6810
	// Next, check the ssl stream context for certificate info
6811
	if (version_compare(PHP_VERSION, '5.6.0', '<'))
6812
		$ssloptions = array("capture_peer_cert" => true);
6813
	else
6814
		$ssloptions = array("capture_peer_cert" => true, "verify_peer" => true, "allow_self_signed" => true);
6815
6816
	$result = false;
6817
	$context = stream_context_create(array("ssl" => $ssloptions));
6818
	$stream = @stream_socket_client($url, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
6819
	if ($stream !== false)
6820
	{
6821
		$params = stream_context_get_params($stream);
6822
		$result = isset($params["options"]["ssl"]["peer_certificate"]) ? true : false;
6823
	}
6824
	return $result;
6825
}
6826
6827
/**
6828
 * Check if the passed url has a redirect to https:// by querying headers.
6829
 *
6830
 * Returns true if a redirect was found & false if not.
6831
 * Note that when force_ssl = 2, SMF issues its own redirect...  So if this
6832
 * returns true, it may be caused by SMF, not necessarily an .htaccess redirect.
6833
 *
6834
 * @param string $url to check, in $boardurl format (no trailing slash).
6835
 */
6836
function https_redirect_active($url)
6837
{
6838
	// Ask for the headers for the passed url, but via http...
6839
	// Need to add the trailing slash, or it puts it there & thinks there's a redirect when there isn't...
6840
	$url = str_ireplace('https://', 'http://', $url) . '/';
6841
	$headers = @get_headers($url);
6842
	if ($headers === false)
6843
		return false;
6844
6845
	// Now to see if it came back https...
6846
	// First check for a redirect status code in first row (301, 302, 307)
6847
	if (strstr($headers[0], '301') === false && strstr($headers[0], '302') === false && strstr($headers[0], '307') === false)
6848
		return false;
6849
6850
	// Search for the location entry to confirm https
6851
	$result = false;
6852
	foreach ($headers as $header)
6853
	{
6854
		if (stristr($header, 'Location: https://') !== false)
6855
		{
6856
			$result = true;
6857
			break;
6858
		}
6859
	}
6860
	return $result;
6861
}
6862
6863
/**
6864
 * Build query_wanna_see_board and query_see_board for a userid
6865
 *
6866
 * Returns array with keys query_wanna_see_board and query_see_board
6867
 *
6868
 * @param int $userid of the user
6869
 */
6870
function build_query_board($userid)
6871
{
6872
	global $user_info, $modSettings, $smcFunc, $db_prefix;
6873
6874
	$query_part = array();
6875
6876
	// If we come from cron, we can't have a $user_info.
6877
	if (isset($user_info['id']) && $user_info['id'] == $userid && SMF != 'BACKGROUND')
6878
	{
6879
		$groups = $user_info['groups'];
6880
		$can_see_all_boards = $user_info['is_admin'] || $user_info['can_manage_boards'];
6881
		$ignoreboards = !empty($user_info['ignoreboards']) ? $user_info['ignoreboards'] : null;
6882
	}
6883
	else
6884
	{
6885
		$request = $smcFunc['db_query']('', '
6886
			SELECT mem.ignore_boards, mem.id_group, mem.additional_groups, mem.id_post_group
6887
			FROM {db_prefix}members AS mem
6888
			WHERE mem.id_member = {int:id_member}
6889
			LIMIT 1',
6890
			array(
6891
				'id_member' => $userid,
6892
			)
6893
		);
6894
6895
		$row = $smcFunc['db_fetch_assoc']($request);
6896
6897
		if (empty($row['additional_groups']))
6898
			$groups = array($row['id_group'], $row['id_post_group']);
6899
		else
6900
			$groups = array_merge(
6901
				array($row['id_group'], $row['id_post_group']),
6902
				explode(',', $row['additional_groups'])
6903
			);
6904
6905
		// Because history has proven that it is possible for groups to go bad - clean up in case.
6906
		foreach ($groups as $k => $v)
6907
			$groups[$k] = (int) $v;
6908
6909
		$can_see_all_boards = in_array(1, $groups) || (!empty($modSettings['board_manager_groups']) && count(array_intersect($groups, explode(',', $modSettings['board_manager_groups']))) > 0);
6910
6911
		$ignoreboards = !empty($row['ignore_boards']) && !empty($modSettings['allow_ignore_boards']) ? explode(',', $row['ignore_boards']) : array();
6912
	}
6913
6914
	// Just build this here, it makes it easier to change/use - administrators can see all boards.
6915
	if ($can_see_all_boards)
6916
		$query_part['query_see_board'] = '1=1';
6917
	// Otherwise just the groups in $user_info['groups'].
6918
	else
6919
	{
6920
		$query_part['query_see_board'] = '
6921
			EXISTS (
6922
				SELECT bpv.id_board
6923
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
6924
				WHERE bpv.id_group IN ('. implode(',', $groups) .')
6925
					AND bpv.deny = 0
6926
					AND bpv.id_board = b.id_board
6927
			)';
6928
6929
		if (!empty($modSettings['deny_boards_access']))
6930
			$query_part['query_see_board'] .= '
6931
			AND NOT EXISTS (
6932
				SELECT bpv.id_board
6933
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
6934
				WHERE bpv.id_group IN ( '. implode(',', $groups) .')
6935
					AND bpv.deny = 1
6936
					AND bpv.id_board = b.id_board
6937
			)';
6938
	}
6939
6940
	$query_part['query_see_message_board'] = str_replace('b.', 'm.', $query_part['query_see_board']);
6941
	$query_part['query_see_topic_board'] = str_replace('b.', 't.', $query_part['query_see_board']);
6942
6943
	// Build the list of boards they WANT to see.
6944
	// This will take the place of query_see_boards in certain spots, so it better include the boards they can see also
6945
6946
	// If they aren't ignoring any boards then they want to see all the boards they can see
6947
	if (empty($ignoreboards))
6948
	{
6949
		$query_part['query_wanna_see_board'] = $query_part['query_see_board'];
6950
		$query_part['query_wanna_see_message_board'] = $query_part['query_see_message_board'];
6951
		$query_part['query_wanna_see_topic_board'] = $query_part['query_see_topic_board'];
6952
	}
6953
	// Ok I guess they don't want to see all the boards
6954
	else
6955
	{
6956
		$query_part['query_wanna_see_board'] = '(' . $query_part['query_see_board'] . ' AND b.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
6957
		$query_part['query_wanna_see_message_board'] = '(' . $query_part['query_see_message_board'] . ' AND m.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
6958
		$query_part['query_wanna_see_topic_board'] = '(' . $query_part['query_see_topic_board'] . ' AND t.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
6959
	}
6960
6961
	return $query_part;
6962
}
6963
6964
/**
6965
 * Check if the connection is using https.
6966
 *
6967
 * @return boolean true if connection used https
6968
 */
6969
function httpsOn()
6970
{
6971
	$secure = false;
6972
6973
	if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on')
6974
		$secure = true;
6975
	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...
6976
		$secure = true;
6977
6978
	return $secure;
6979
}
6980
6981
/**
6982
 * A wrapper for `filter_var($url, FILTER_VALIDATE_URL)` that can handle URLs
6983
 * with international characters (a.k.a. IRIs)
6984
 *
6985
 * @param string $iri The IRI to test.
6986
 * @param int $flags Optional flags to pass to filter_var()
6987
 * @return string|bool Either the original IRI, or false if the IRI was invalid.
6988
 */
6989
function validate_iri($iri, $flags = null)
6990
{
6991
	$url = iri_to_url($iri);
6992
6993
	if (filter_var($url, FILTER_VALIDATE_URL, $flags) !== false)
6994
		return $iri;
6995
	else
6996
		return false;
6997
}
6998
6999
/**
7000
 * A wrapper for `filter_var($url, FILTER_SANITIZE_URL)` that can handle URLs
7001
 * with international characters (a.k.a. IRIs)
7002
 *
7003
 * Note: The returned value will still be an IRI, not a URL. To convert to URL,
7004
 * feed the result of this function to iri_to_url()
7005
 *
7006
 * @param string $iri The IRI to sanitize.
7007
 * @return string|bool The sanitized version of the IRI
7008
 */
7009
function sanitize_iri($iri)
7010
{
7011
	// Encode any non-ASCII characters (but not space or control characters of any sort)
7012
	$iri = preg_replace_callback('~[^\x00-\x7F\pZ\pC]~u', function($matches)
7013
	{
7014
		return rawurlencode($matches[0]);
7015
	}, $iri);
7016
7017
	// Perform normal sanitization
7018
	$iri = filter_var($iri, FILTER_SANITIZE_URL);
7019
7020
	// Decode the non-ASCII characters
7021
	$iri = rawurldecode($iri);
7022
7023
	return $iri;
7024
}
7025
7026
/**
7027
 * Converts a URL with international characters (an IRI) into a pure ASCII URL
7028
 *
7029
 * Uses Punycode to encode any non-ASCII characters in the domain name, and uses
7030
 * standard URL encoding on the rest.
7031
 *
7032
 * @param string $iri A IRI that may or may not contain non-ASCII characters.
7033
 * @return string|bool The URL version of the IRI.
7034
 */
7035
function iri_to_url($iri)
7036
{
7037
	global $sourcedir;
7038
7039
	$host = parse_url((strpos($iri, '://') === false ? 'http://' : '') . ltrim($iri, ':/'), PHP_URL_HOST);
7040
7041
	if (empty($host))
7042
		return $iri;
7043
7044
	// Convert the domain using the Punycode algorithm
7045
	require_once($sourcedir . '/Class-Punycode.php');
7046
	$Punycode = new Punycode();
7047
	$encoded_host = $Punycode->encode($host);
7048
	$pos = strpos($iri, $host);
7049
	$iri = substr_replace($iri, $encoded_host, $pos, strlen($host));
7050
7051
	// Encode any disallowed characters in the rest of the URL
7052
	$unescaped = array(
7053
		'%21' => '!', '%23' => '#', '%24' => '$', '%26' => '&',
7054
		'%27' => "'", '%28' => '(', '%29' => ')', '%2A' => '*',
7055
		'%2B' => '+', '%2C' => ',', '%2F' => '/', '%3A' => ':',
7056
		'%3B' => ';', '%3D' => '=', '%3F' => '?', '%40' => '@',
7057
		'%25' => '%',
7058
	);
7059
	$iri = strtr(rawurlencode($iri), $unescaped);
7060
7061
	return $iri;
7062
}
7063
7064
/**
7065
 * Decodes a URL containing encoded international characters to UTF-8
7066
 *
7067
 * Decodes any Punycode encoded characters in the domain name, then uses
7068
 * standard URL decoding on the rest.
7069
 *
7070
 * @param string $url The pure ASCII version of a URL.
7071
 * @return string|bool The UTF-8 version of the URL.
7072
 */
7073
function url_to_iri($url)
7074
{
7075
	global $sourcedir;
7076
7077
	$host = parse_url((strpos($url, '://') === false ? 'http://' : '') . ltrim($url, ':/'), PHP_URL_HOST);
7078
7079
	if (empty($host))
7080
		return $url;
7081
7082
	// Decode the domain from Punycode
7083
	require_once($sourcedir . '/Class-Punycode.php');
7084
	$Punycode = new Punycode();
7085
	$decoded_host = $Punycode->decode($host);
7086
	$pos = strpos($url, $host);
7087
	$url = substr_replace($url, $decoded_host, $pos, strlen($host));
7088
7089
	// Decode the rest of the URL
7090
	$url = rawurldecode($url);
7091
7092
	return $url;
7093
}
7094
7095
/**
7096
 * Ensures SMF's scheduled tasks are being run as intended
7097
 *
7098
 * If the admin activated the cron_is_real_cron setting, but the cron job is
7099
 * not running things at least once per day, we need to go back to SMF's default
7100
 * behaviour using "web cron" JavaScript calls.
7101
 */
7102
function check_cron()
7103
{
7104
	global $modSettings, $smcFunc, $txt;
7105
7106
	if (!empty($modSettings['cron_is_real_cron']) && time() - @intval($modSettings['cron_last_checked']) > 84600)
7107
	{
7108
		$request = $smcFunc['db_query']('', '
7109
			SELECT COUNT(*)
7110
			FROM {db_prefix}scheduled_tasks
7111
			WHERE disabled = {int:not_disabled}
7112
				AND next_time < {int:yesterday}',
7113
			array(
7114
				'not_disabled' => 0,
7115
				'yesterday' => time() - 84600,
7116
			)
7117
		);
7118
		list($overdue) = $smcFunc['db_fetch_row']($request);
7119
		$smcFunc['db_free_result']($request);
7120
7121
		// If we have tasks more than a day overdue, cron isn't doing its job.
7122
		if (!empty($overdue))
7123
		{
7124
			loadLanguage('ManageScheduledTasks');
7125
			log_error($txt['cron_not_working']);
7126
			updateSettings(array('cron_is_real_cron' => 0));
7127
		}
7128
		else
7129
			updateSettings(array('cron_last_checked' => time()));
7130
	}
7131
}
7132
7133
/**
7134
 * Sends an appropriate HTTP status header based on a given status code
7135
 *
7136
 * @param int $code The status code
7137
 * @param string $status The string for the status. Set automatically if not provided.
7138
 */
7139
function send_http_status($code, $status = '')
7140
{
7141
	$statuses = array(
7142
		206 => 'Partial Content',
7143
		304 => 'Not Modified',
7144
		400 => 'Bad Request',
7145
		403 => 'Forbidden',
7146
		404 => 'Not Found',
7147
		410 => 'Gone',
7148
		500 => 'Internal Server Error',
7149
		503 => 'Service Unavailable',
7150
	);
7151
7152
	$protocol = preg_match('~^\s*(HTTP/[12]\.\d)\s*$~i', $_SERVER['SERVER_PROTOCOL'], $matches) ? $matches[1] : 'HTTP/1.0';
7153
7154
	if (!isset($statuses[$code]) && empty($status))
7155
		header($protocol . ' 500 Internal Server Error');
7156
	else
7157
		header($protocol . ' ' . $code . ' ' . (!empty($status) ? $status : $statuses[$code]));
7158
}
7159
7160
/**
7161
 * Concatenates an array of strings into a grammatically correct sentence list
7162
 *
7163
 * Uses formats defined in the language files to build the list appropropriately
7164
 * for the currently loaded language.
7165
 *
7166
 * @param array $list An array of strings to concatenate.
7167
 * @return string The localized sentence list.
7168
 */
7169
function sentence_list($list)
7170
{
7171
	global $txt;
7172
7173
	// Make sure the bare necessities are defined
7174
	if (empty($txt['sentence_list_format']['n']))
7175
		$txt['sentence_list_format']['n'] = '{series}';
7176
	if (!isset($txt['sentence_list_separator']))
7177
		$txt['sentence_list_separator'] = ', ';
7178
	if (!isset($txt['sentence_list_separator_alt']))
7179
		$txt['sentence_list_separator_alt'] = '; ';
7180
7181
	// Which format should we use?
7182
	if (isset($txt['sentence_list_format'][count($list)]))
7183
		$format = $txt['sentence_list_format'][count($list)];
7184
	else
7185
		$format = $txt['sentence_list_format']['n'];
7186
7187
	// Do we want the normal separator or the alternate?
7188
	$separator = $txt['sentence_list_separator'];
7189
	foreach ($list as $item)
7190
	{
7191
		if (strpos($item, $separator) !== false)
7192
		{
7193
			$separator = $txt['sentence_list_separator_alt'];
7194
			$format = strtr($format, trim($txt['sentence_list_separator']), trim($separator));
7195
			break;
7196
		}
7197
	}
7198
7199
	$replacements = array();
7200
7201
	// Special handling for the last items on the list
7202
	$i = 0;
7203
	while (empty($done))
7204
	{
7205
		if (strpos($format, '{'. --$i . '}') !== false)
7206
			$replacements['{'. $i . '}'] = array_pop($list);
7207
		else
7208
			$done = true;
7209
	}
7210
	unset($done);
7211
7212
	// Special handling for the first items on the list
7213
	$i = 0;
7214
	while (empty($done))
7215
	{
7216
		if (strpos($format, '{'. ++$i . '}') !== false)
7217
			$replacements['{'. $i . '}'] = array_shift($list);
7218
		else
7219
			$done = true;
7220
	}
7221
	unset($done);
7222
7223
	// Whatever is left
7224
	$replacements['{series}'] = implode($separator, $list);
7225
7226
	// Do the deed
7227
	return strtr($format, $replacements);
7228
}
7229
7230
/**
7231
 * Truncate an array to a specified length
7232
 *
7233
 * @param array $array The array to truncate
7234
 * @param int $max_length The upperbound on the length
7235
 * @param int $deep How levels in an multidimensional array should the function take into account.
7236
 * @return array The truncated array
7237
 */
7238
function truncate_array($array, $max_length = 1900, $deep = 3)
7239
{
7240
    $array = (array) $array;
7241
7242
    $curr_length = array_length($array, $deep);
7243
7244
    if ($curr_length <= $max_length)
7245
        return $array;
7246
7247
    else
7248
    {
7249
        // Truncate each element's value to a reasonable length
7250
        $param_max = floor($max_length / count($array));
7251
7252
        $current_deep = $deep - 1;
7253
7254
        foreach ($array as $key => &$value)
7255
        {
7256
            if (is_array($value))
7257
                if ($current_deep > 0)
7258
                    $value = truncate_array($value, $current_deep);
7259
7260
            else
7261
                $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 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

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

7261
                $value = substr(/** @scrutinizer ignore-type */ $value, 0, $param_max - strlen($key) - 5);
Loading history...
7262
        }
7263
7264
        return $array;
7265
    }
7266
}
7267
7268
/**
7269
 * array_length Recursive
7270
 * @param $array
7271
 * @param int $deep How many levels should the function
7272
 * @return int
7273
 */
7274
function array_length($array, $deep = 3)
7275
{
7276
    // Work with arrays
7277
    $array = (array) $array;
7278
    $length = 0;
7279
7280
    $deep_count = $deep - 1;
7281
7282
    foreach ($array as $value)
7283
    {
7284
        // Recursive?
7285
        if (is_array($value))
7286
        {
7287
            // No can't do
7288
            if ($deep_count <= 0)
7289
                continue;
7290
7291
            $length += array_length($value, $deep_count);
7292
        }
7293
7294
        else
7295
            $length += strlen($value);
7296
    }
7297
7298
    return $length;
7299
}
7300
7301
?>