Passed
Pull Request — release-2.1 (#5958)
by John
03:35
created

get_gravatar_url()   B

Complexity

Conditions 10

Size

Total Lines 27
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 18
nop 1
dl 0
loc 27
rs 7.6666
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
	elseif ($members === null)
283
		$condition = '1=1';
284
285
	else
286
	{
287
		$condition = 'id_member = {int:member}';
288
		$parameters['member'] = $members;
289
	}
290
291
	// Everything is assumed to be a string unless it's in the below.
292
	$knownInts = array(
293
		'date_registered', 'posts', 'id_group', 'last_login', 'instant_messages', 'unread_messages',
294
		'new_pm', 'pm_prefs', 'gender', 'show_online', 'pm_receive_from', 'alerts',
295
		'id_theme', 'is_activated', 'id_msg_last_visit', 'id_post_group', 'total_time_logged_in', 'warning',
296
	);
297
	$knownFloats = array(
298
		'time_offset',
299
	);
300
301
	if (!empty($modSettings['integrate_change_member_data']))
302
	{
303
		// Only a few member variables are really interesting for integration.
304
		$integration_vars = array(
305
			'member_name',
306
			'real_name',
307
			'email_address',
308
			'id_group',
309
			'gender',
310
			'birthdate',
311
			'website_title',
312
			'website_url',
313
			'location',
314
			'time_format',
315
			'time_offset',
316
			'avatar',
317
			'lngfile',
318
		);
319
		$vars_to_integrate = array_intersect($integration_vars, array_keys($data));
320
321
		// Only proceed if there are any variables left to call the integration function.
322
		if (count($vars_to_integrate) != 0)
323
		{
324
			// Fetch a list of member_names if necessary
325
			if ((!is_array($members) && $members === $user_info['id']) || (is_array($members) && count($members) == 1 && in_array($user_info['id'], $members)))
326
				$member_names = array($user_info['username']);
327
			else
328
			{
329
				$member_names = array();
330
				$request = $smcFunc['db_query']('', '
331
					SELECT member_name
332
					FROM {db_prefix}members
333
					WHERE ' . $condition,
334
					$parameters
335
				);
336
				while ($row = $smcFunc['db_fetch_assoc']($request))
337
					$member_names[] = $row['member_name'];
338
				$smcFunc['db_free_result']($request);
339
			}
340
341
			if (!empty($member_names))
342
				foreach ($vars_to_integrate as $var)
343
					call_integration_hook('integrate_change_member_data', array($member_names, $var, &$data[$var], &$knownInts, &$knownFloats));
344
		}
345
	}
346
347
	$setString = '';
348
	foreach ($data as $var => $val)
349
	{
350
		switch ($var)
351
		{
352
			case  'birthdate':
353
				$type = 'date';
354
				break;
355
356
			case 'member_ip':
357
			case 'member_ip2':
358
				$type = 'inet';
359
				break;
360
361
			default:
362
				$type = 'string';
363
		}
364
365
		if (in_array($var, $knownInts))
366
			$type = 'int';
367
368
		elseif (in_array($var, $knownFloats))
369
			$type = 'float';
370
371
		// Doing an increment?
372
		if ($var == 'alerts' && ($val === '+' || $val === '-'))
373
		{
374
			include_once($sourcedir . '/Profile-Modify.php');
375
			if (is_array($members))
376
			{
377
				$val = 'CASE ';
378
				foreach ($members as $k => $v)
379
					$val .= 'WHEN id_member = ' . $v . ' THEN ' . alert_count($v, true) . ' ';
380
381
				$val = $val . ' END';
382
				$type = 'raw';
383
			}
384
			else
385
				$val = alert_count($members, true);
386
		}
387
		elseif ($type == 'int' && ($val === '+' || $val === '-'))
388
		{
389
			$val = $var . ' ' . $val . ' 1';
390
			$type = 'raw';
391
		}
392
393
		// Ensure posts, instant_messages, and unread_messages don't overflow or underflow.
394
		if (in_array($var, array('posts', 'instant_messages', 'unread_messages')))
395
		{
396
			if (preg_match('~^' . $var . ' (\+ |- |\+ -)([\d]+)~', $val, $match))
397
			{
398
				if ($match[1] != '+ ')
399
					$val = 'CASE WHEN ' . $var . ' <= ' . abs($match[2]) . ' THEN 0 ELSE ' . $val . ' END';
400
401
				$type = 'raw';
402
			}
403
		}
404
405
		$setString .= ' ' . $var . ' = {' . $type . ':p_' . $var . '},';
406
		$parameters['p_' . $var] = $val;
407
	}
408
409
	$smcFunc['db_query']('', '
410
		UPDATE {db_prefix}members
411
		SET' . substr($setString, 0, -1) . '
412
		WHERE ' . $condition,
413
		$parameters
414
	);
415
416
	updateStats('postgroups', $members, array_keys($data));
417
418
	// Clear any caching?
419
	if (!empty($cache_enable) && $cache_enable >= 2 && !empty($members))
420
	{
421
		if (!is_array($members))
422
			$members = array($members);
423
424
		foreach ($members as $member)
425
		{
426
			if ($cache_enable >= 3)
427
			{
428
				cache_put_data('member_data-profile-' . $member, null, 120);
429
				cache_put_data('member_data-normal-' . $member, null, 120);
430
				cache_put_data('member_data-minimal-' . $member, null, 120);
431
			}
432
			cache_put_data('user_settings-' . $member, null, 60);
433
		}
434
	}
435
}
436
437
/**
438
 * Updates the settings table as well as $modSettings... only does one at a time if $update is true.
439
 *
440
 * - updates both the settings table and $modSettings array.
441
 * - all of changeArray's indexes and values are assumed to have escaped apostrophes (')!
442
 * - if a variable is already set to what you want to change it to, that
443
 *   variable will be skipped over; it would be unnecessary to reset.
444
 * - When use_update is true, UPDATEs will be used instead of REPLACE.
445
 * - when use_update is true, the value can be true or false to increment
446
 *  or decrement it, respectively.
447
 *
448
 * @param array $changeArray An array of info about what we're changing in 'setting' => 'value' format
449
 * @param bool $update Whether to use an UPDATE query instead of a REPLACE query
450
 */
451
function updateSettings($changeArray, $update = false)
452
{
453
	global $modSettings, $smcFunc;
454
455
	if (empty($changeArray) || !is_array($changeArray))
456
		return;
457
458
	$toRemove = array();
459
460
	// Go check if there is any setting to be removed.
461
	foreach ($changeArray as $k => $v)
462
		if ($v === null)
463
		{
464
			// Found some, remove them from the original array and add them to ours.
465
			unset($changeArray[$k]);
466
			$toRemove[] = $k;
467
		}
468
469
	// Proceed with the deletion.
470
	if (!empty($toRemove))
471
		$smcFunc['db_query']('', '
472
			DELETE FROM {db_prefix}settings
473
			WHERE variable IN ({array_string:remove})',
474
			array(
475
				'remove' => $toRemove,
476
			)
477
		);
478
479
	// In some cases, this may be better and faster, but for large sets we don't want so many UPDATEs.
480
	if ($update)
481
	{
482
		foreach ($changeArray as $variable => $value)
483
		{
484
			$smcFunc['db_query']('', '
485
				UPDATE {db_prefix}settings
486
				SET value = {' . ($value === false || $value === true ? 'raw' : 'string') . ':value}
487
				WHERE variable = {string:variable}',
488
				array(
489
					'value' => $value === true ? 'value + 1' : ($value === false ? 'value - 1' : $value),
490
					'variable' => $variable,
491
				)
492
			);
493
			$modSettings[$variable] = $value === true ? $modSettings[$variable] + 1 : ($value === false ? $modSettings[$variable] - 1 : $value);
494
		}
495
496
		// Clean out the cache and make sure the cobwebs are gone too.
497
		cache_put_data('modSettings', null, 90);
498
499
		return;
500
	}
501
502
	$replaceArray = array();
503
	foreach ($changeArray as $variable => $value)
504
	{
505
		// Don't bother if it's already like that ;).
506
		if (isset($modSettings[$variable]) && $modSettings[$variable] == $value)
507
			continue;
508
		// If the variable isn't set, but would only be set to nothing'ness, then don't bother setting it.
509
		elseif (!isset($modSettings[$variable]) && empty($value))
510
			continue;
511
512
		$replaceArray[] = array($variable, $value);
513
514
		$modSettings[$variable] = $value;
515
	}
516
517
	if (empty($replaceArray))
518
		return;
519
520
	$smcFunc['db_insert']('replace',
521
		'{db_prefix}settings',
522
		array('variable' => 'string-255', 'value' => 'string-65534'),
523
		$replaceArray,
524
		array('variable')
525
	);
526
527
	// Kill the cache - it needs redoing now, but we won't bother ourselves with that here.
528
	cache_put_data('modSettings', null, 90);
529
}
530
531
/**
532
 * Constructs a page list.
533
 *
534
 * - builds the page list, e.g. 1 ... 6 7 [8] 9 10 ... 15.
535
 * - flexible_start causes it to use "url.page" instead of "url;start=page".
536
 * - very importantly, cleans up the start value passed, and forces it to
537
 *   be a multiple of num_per_page.
538
 * - checks that start is not more than max_value.
539
 * - base_url should be the URL without any start parameter on it.
540
 * - uses the compactTopicPagesEnable and compactTopicPagesContiguous
541
 *   settings to decide how to display the menu.
542
 *
543
 * an example is available near the function definition.
544
 * $pageindex = constructPageIndex($scripturl . '?board=' . $board, $_REQUEST['start'], $num_messages, $maxindex, true);
545
 *
546
 * @param string $base_url The basic URL to be used for each link.
547
 * @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.
548
 * @param int $max_value The total number of items you are paginating for.
549
 * @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.
550
 * @param bool $flexible_start Whether a ;start=x component should be introduced into the URL automatically (see above)
551
 * @param bool $show_prevnext Whether the Previous and Next links should be shown (should be on only when navigating the list)
552
 *
553
 * @return string The complete HTML of the page index that was requested, formatted by the template.
554
 */
555
function constructPageIndex($base_url, &$start, $max_value, $num_per_page, $flexible_start = false, $show_prevnext = true)
556
{
557
	global $modSettings, $context, $smcFunc, $settings, $txt;
558
559
	// Save whether $start was less than 0 or not.
560
	$start = (int) $start;
561
	$start_invalid = $start < 0;
562
563
	// Make sure $start is a proper variable - not less than 0.
564
	if ($start_invalid)
565
		$start = 0;
566
	// Not greater than the upper bound.
567
	elseif ($start >= $max_value)
568
		$start = max(0, (int) $max_value - (((int) $max_value % (int) $num_per_page) == 0 ? $num_per_page : ((int) $max_value % (int) $num_per_page)));
569
	// And it has to be a multiple of $num_per_page!
570
	else
571
		$start = max(0, (int) $start - ((int) $start % (int) $num_per_page));
572
573
	$context['current_page'] = $start / $num_per_page;
574
575
	// Define some default page index settings if we don't already have it...
576
	if (!isset($settings['page_index']))
577
	{
578
		// This defines the formatting for the page indexes used throughout the forum.
579
		$settings['page_index'] = array(
580
			'extra_before' => '<span class="pages">' . $txt['pages'] . '</span>',
581
			'previous_page' => '<span class="main_icons previous_page"></span>',
582
			'current_page' => '<span class="current_page">%1$d</span> ',
583
			'page' => '<a class="nav_page" href="{URL}">%2$s</a> ',
584
			'expand_pages' => '<span class="expand_pages" onclick="expandPages(this, {LINK}, {FIRST_PAGE}, {LAST_PAGE}, {PER_PAGE});"> ... </span>',
585
			'next_page' => '<span class="main_icons next_page"></span>',
586
			'extra_after' => '',
587
		);
588
	}
589
590
	$base_link = strtr($settings['page_index']['page'], array('{URL}' => $flexible_start ? $base_url : strtr($base_url, array('%' => '%%')) . ';start=%1$d'));
591
	$pageindex = $settings['page_index']['extra_before'];
592
593
	// Compact pages is off or on?
594
	if (empty($modSettings['compactTopicPagesEnable']))
595
	{
596
		// Show the left arrow.
597
		$pageindex .= $start == 0 ? ' ' : sprintf($base_link, $start - $num_per_page, $settings['page_index']['previous_page']);
598
599
		// Show all the pages.
600
		$display_page = 1;
601
		for ($counter = 0; $counter < $max_value; $counter += $num_per_page)
602
			$pageindex .= $start == $counter && !$start_invalid ? sprintf($settings['page_index']['current_page'], $display_page++) : sprintf($base_link, $counter, $display_page++);
603
604
		// Show the right arrow.
605
		$display_page = ($start + $num_per_page) > $max_value ? $max_value : ($start + $num_per_page);
606
		if ($start != $counter - $max_value && !$start_invalid)
607
			$pageindex .= $display_page > $counter - $num_per_page ? ' ' : sprintf($base_link, $display_page, $settings['page_index']['next_page']);
608
	}
609
	else
610
	{
611
		// If they didn't enter an odd value, pretend they did.
612
		$PageContiguous = (int) ($modSettings['compactTopicPagesContiguous'] - ($modSettings['compactTopicPagesContiguous'] % 2)) / 2;
613
614
		// Show the "prev page" link. (>prev page< 1 ... 6 7 [8] 9 10 ... 15 next page)
615
		if (!empty($start) && $show_prevnext)
616
			$pageindex .= sprintf($base_link, $start - $num_per_page, $settings['page_index']['previous_page']);
617
		else
618
			$pageindex .= '';
619
620
		// Show the first page. (prev page >1< ... 6 7 [8] 9 10 ... 15)
621
		if ($start > $num_per_page * $PageContiguous)
622
			$pageindex .= sprintf($base_link, 0, '1');
623
624
		// Show the ... after the first page.  (prev page 1 >...< 6 7 [8] 9 10 ... 15 next page)
625
		if ($start > $num_per_page * ($PageContiguous + 1))
626
			$pageindex .= strtr($settings['page_index']['expand_pages'], array(
627
				'{LINK}' => JavaScriptEscape($smcFunc['htmlspecialchars']($base_link)),
628
				'{FIRST_PAGE}' => $num_per_page,
629
				'{LAST_PAGE}' => $start - $num_per_page * $PageContiguous,
630
				'{PER_PAGE}' => $num_per_page,
631
			));
632
633
		// Show the pages before the current one. (prev page 1 ... >6 7< [8] 9 10 ... 15 next page)
634
		for ($nCont = $PageContiguous; $nCont >= 1; $nCont--)
635
			if ($start >= $num_per_page * $nCont)
636
			{
637
				$tmpStart = $start - $num_per_page * $nCont;
638
				$pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
639
			}
640
641
		// Show the current page. (prev page 1 ... 6 7 >[8]< 9 10 ... 15 next page)
642
		if (!$start_invalid)
643
			$pageindex .= sprintf($settings['page_index']['current_page'], $start / $num_per_page + 1);
644
		else
645
			$pageindex .= sprintf($base_link, $start, $start / $num_per_page + 1);
646
647
		// Show the pages after the current one... (prev page 1 ... 6 7 [8] >9 10< ... 15 next page)
648
		$tmpMaxPages = (int) (($max_value - 1) / $num_per_page) * $num_per_page;
649
		for ($nCont = 1; $nCont <= $PageContiguous; $nCont++)
650
			if ($start + $num_per_page * $nCont <= $tmpMaxPages)
651
			{
652
				$tmpStart = $start + $num_per_page * $nCont;
653
				$pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
654
			}
655
656
		// Show the '...' part near the end. (prev page 1 ... 6 7 [8] 9 10 >...< 15 next page)
657
		if ($start + $num_per_page * ($PageContiguous + 1) < $tmpMaxPages)
658
			$pageindex .= strtr($settings['page_index']['expand_pages'], array(
659
				'{LINK}' => JavaScriptEscape($smcFunc['htmlspecialchars']($base_link)),
660
				'{FIRST_PAGE}' => $start + $num_per_page * ($PageContiguous + 1),
661
				'{LAST_PAGE}' => $tmpMaxPages,
662
				'{PER_PAGE}' => $num_per_page,
663
			));
664
665
		// Show the last number in the list. (prev page 1 ... 6 7 [8] 9 10 ... >15<  next page)
666
		if ($start + $num_per_page * $PageContiguous < $tmpMaxPages)
667
			$pageindex .= sprintf($base_link, $tmpMaxPages, $tmpMaxPages / $num_per_page + 1);
668
669
		// Show the "next page" link. (prev page 1 ... 6 7 [8] 9 10 ... 15 >next page<)
670
		if ($start != $tmpMaxPages && $show_prevnext)
671
			$pageindex .= sprintf($base_link, $start + $num_per_page, $settings['page_index']['next_page']);
672
	}
673
	$pageindex .= $settings['page_index']['extra_after'];
674
675
	return $pageindex;
676
}
677
678
/**
679
 * - Formats a number.
680
 * - uses the format of number_format to decide how to format the number.
681
 *   for example, it might display "1 234,50".
682
 * - caches the formatting data from the setting for optimization.
683
 *
684
 * @param float $number A number
685
 * @param bool|int $override_decimal_count If set, will use the specified number of decimal places. Otherwise it's automatically determined
686
 * @return string A formatted number
687
 */
688
function comma_format($number, $override_decimal_count = false)
689
{
690
	global $txt;
691
	static $thousands_separator = null, $decimal_separator = null, $decimal_count = null;
692
693
	// Cache these values...
694
	if ($decimal_separator === null)
695
	{
696
		// Not set for whatever reason?
697
		if (empty($txt['number_format']) || preg_match('~^1([^\d]*)?234([^\d]*)(0*?)$~', $txt['number_format'], $matches) != 1)
698
			return $number;
699
700
		// Cache these each load...
701
		$thousands_separator = $matches[1];
702
		$decimal_separator = $matches[2];
703
		$decimal_count = strlen($matches[3]);
704
	}
705
706
	// Format the string with our friend, number_format.
707
	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

707
	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...
708
}
709
710
/**
711
 * Format a time to make it look purdy.
712
 *
713
 * - returns a pretty formatted version of time based on the user's format in $user_info['time_format'].
714
 * - applies all necessary time offsets to the timestamp, unless offset_type is set.
715
 * - if todayMod is set and show_today was not not specified or true, an
716
 *   alternate format string is used to show the date with something to show it is "today" or "yesterday".
717
 * - performs localization (more than just strftime would do alone.)
718
 *
719
 * @param int $log_time A timestamp
720
 * @param bool $show_today Whether to show "Today"/"Yesterday" or just a date
721
 * @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.
722
 * @param bool $process_safe Activate setlocale check for changes at runtime. Slower, but safer.
723
 * @return string A formatted timestamp
724
 */
725
function timeformat($log_time, $show_today = true, $offset_type = false, $process_safe = false)
726
{
727
	global $context, $user_info, $txt, $modSettings;
728
	static $non_twelve_hour, $locale, $now;
729
	static $unsupportedFormats, $finalizedFormats;
730
731
	$unsupportedFormatsWindows = array('z', 'Z');
732
733
	// Ensure required values are set
734
	$user_info['time_offset'] = !empty($user_info['time_offset']) ? $user_info['time_offset'] : 0;
735
	$modSettings['time_offset'] = !empty($modSettings['time_offset']) ? $modSettings['time_offset'] : 0;
736
	$user_info['time_format'] = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %H:%M');
737
738
	// Offset the time.
739
	if (!$offset_type)
740
		$log_time = $log_time + ($user_info['time_offset'] + $modSettings['time_offset']) * 3600;
741
	// Just the forum offset?
742
	elseif ($offset_type == 'forum')
743
		$log_time = $log_time + $modSettings['time_offset'] * 3600;
744
745
	// We can't have a negative date (on Windows, at least.)
746
	if ($log_time < 0)
747
		$log_time = 0;
748
749
	// Today and Yesterday?
750
	$prefix = '';
751
	if ($modSettings['todayMod'] >= 1 && $show_today === true)
752
	{
753
		$now_time = forum_time();
754
755
		if ($now_time - $log_time < (86400 * $modSettings['todayMod']))
756
		{
757
			$then = @getdate($log_time);
758
			$now = (!empty($now) ? $now : @getdate($now_time));
759
760
			// Same day of the year, same year.... Today!
761
			if ($then['yday'] == $now['yday'] && $then['year'] == $now['year'])
762
			{
763
				$prefix = $txt['today'];
764
			}
765
			// 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...
766
			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))
767
			{
768
				$prefix = $txt['yesterday'];
769
			}
770
		}
771
	}
772
773
	$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...
774
775
	// Use the cached formats if available
776
	if (is_null($finalizedFormats))
777
		$finalizedFormats = (array) cache_get_data('timeformatstrings', 86400);
778
779
	if (!isset($finalizedFormats[$str]) || !is_array($finalizedFormats[$str]))
780
		$finalizedFormats[$str] = array();
781
782
	// Make a supported version for this format if we don't already have one
783
	$format_type = !empty($prefix) ? 'time_only' : 'normal';
784
	if (empty($finalizedFormats[$str][$format_type]))
785
	{
786
		$timeformat = $format_type == 'time_only' ? get_date_or_time_format('time', $str) : $str;
787
788
		// Not all systems support all formats, and Windows fails altogether if unsupported ones are
789
		// used, so let's prevent that. Some substitutions go to the nearest reasonable fallback, some
790
		// turn into static strings, some (i.e. %a, %A, %b, %B, %p) have special handling below.
791
		$strftimeFormatSubstitutions = array(
792
			// Day
793
			'a' => '#txt_days_short_%w#', 'A' => '#txt_days_%w#', 'e' => '%d', 'd' => '&#37;d', 'j' => '&#37;j', 'u' => '%w', 'w' => '&#37;w',
794
			// Week
795
			'U' => '&#37;U', 'V' => '%U', 'W' => '%U',
796
			// Month
797
			'b' => '#txt_months_short_%m#', 'B' => '#txt_months_%m#', 'h' => '%b', 'm' => '&#37;m',
798
			// Year
799
			'C' => '&#37;C', 'g' => '%y', 'G' => '%Y', 'y' => '&#37;y', 'Y' => '&#37;Y',
800
			// Time
801
			'H' => '&#37;H', 'k' => '%H', 'I' => '%H', 'l' => '%I', 'M' => '&#37;M', 'p' => '&#37;p', 'P' => '%p',
802
			'r' => '%I:%M:%S %p', 'R' => '%H:%M', 'S' => '&#37;S', 'T' => '%H:%M:%S', 'X' => '%T', 'z' => '&#37;z', 'Z' => '&#37;Z',
803
			// Time and Date Stamps
804
			'c' => '%F %T', 'D' => '%m/%d/%y', 'F' => '%Y-%m-%d', 's' => '&#37;s', 'x' => '%F',
805
			// Miscellaneous
806
			'n' => "\n", 't' => "\t", '%' => '&#37;',
807
		);
808
809
		// No need to do this part again if we already did it once
810
		if (is_null($unsupportedFormats))
811
			$unsupportedFormats = (array) cache_get_data('unsupportedtimeformats', 86400);
812
		if (empty($unsupportedFormats))
813
		{
814
			foreach ($strftimeFormatSubstitutions as $format => $substitution)
815
			{
816
				// Avoid a crashing bug with PHP 7 on certain versions of Windows
817
				if ($context['server']['is_windows'] && in_array($format, $unsupportedFormatsWindows))
818
				{
819
					$unsupportedFormats[] = $format;
820
					continue;
821
				}
822
823
				$value = @strftime('%' . $format);
824
825
				// Windows will return false for unsupported formats
826
				// Other operating systems return the format string as a literal
827
				if ($value === false || $value === $format)
828
					$unsupportedFormats[] = $format;
829
			}
830
			cache_put_data('unsupportedtimeformats', $unsupportedFormats, 86400);
831
		}
832
833
		// Windows needs extra help if $timeformat contains something completely invalid, e.g. '%Q'
834
		if (DIRECTORY_SEPARATOR === '\\')
835
			$timeformat = preg_replace('~%(?!' . implode('|', array_keys($strftimeFormatSubstitutions)) . ')~', '&#37;', $timeformat);
836
837
		// Substitute unsupported formats with supported ones
838
		if (!empty($unsupportedFormats))
839
			while (preg_match('~%(' . implode('|', $unsupportedFormats) . ')~', $timeformat, $matches))
840
				$timeformat = str_replace($matches[0], $strftimeFormatSubstitutions[$matches[1]], $timeformat);
841
842
		// Remember this so we don't need to do it again
843
		$finalizedFormats[$str][$format_type] = $timeformat;
844
		cache_put_data('timeformatstrings', $finalizedFormats, 86400);
845
	}
846
847
	$timeformat = $finalizedFormats[$str][$format_type];
848
849
	// Make sure we are using the correct locale.
850
	if (!isset($locale) || ($process_safe === true && setlocale(LC_TIME, '0') != $locale))
851
		$locale = setlocale(LC_TIME, array($txt['lang_locale'] . '.' . $modSettings['global_character_set'], $txt['lang_locale'] . '.' . $txt['lang_character_set'], $txt['lang_locale']));
852
853
	// If the current locale is unsupported, we'll have to localize the hard way.
854
	if ($locale === false)
855
	{
856
		$timeformat = strtr($timeformat, array(
857
			'%a' => '#txt_days_short_%w#',
858
			'%A' => '#txt_days_%w#',
859
			'%b' => '#txt_months_short_%m#',
860
			'%B' => '#txt_months_%m#',
861
			'%p' => '&#37;p',
862
			'%P' => '&#37;p'
863
		));
864
	}
865
	// Just in case the locale doesn't support '%p' properly.
866
	// @todo Is this even necessary?
867
	else
868
	{
869
		if (!isset($non_twelve_hour) && strpos($timeformat, '%p') !== false)
870
			$non_twelve_hour = trim(strftime('%p')) === '';
871
872
		if (!empty($non_twelve_hour))
873
			$timeformat = strtr($timeformat, array(
874
				'%p' => '&#37;p',
875
				'%P' => '&#37;p'
876
			));
877
	}
878
879
	// And now, the moment we've all be waiting for...
880
	$timestring = strftime($timeformat, $log_time);
881
882
	// Do-it-yourself time localization.  Fun.
883
	if (strpos($timestring, '&#37;p') !== false)
884
		$timestring = str_replace('&#37;p', (strftime('%H', $log_time) < 12 ? $txt['time_am'] : $txt['time_pm']), $timestring);
885
	if (strpos($timestring, '#txt_') !== false)
886
	{
887
		if (strpos($timestring, '#txt_days_short_') !== false)
888
			$timestring = strtr($timestring, array(
889
				'#txt_days_short_0#' => $txt['days_short'][0],
890
				'#txt_days_short_1#' => $txt['days_short'][1],
891
				'#txt_days_short_2#' => $txt['days_short'][2],
892
				'#txt_days_short_3#' => $txt['days_short'][3],
893
				'#txt_days_short_4#' => $txt['days_short'][4],
894
				'#txt_days_short_5#' => $txt['days_short'][5],
895
				'#txt_days_short_6#' => $txt['days_short'][6],
896
			));
897
898
		if (strpos($timestring, '#txt_days_') !== false)
899
			$timestring = strtr($timestring, array(
900
				'#txt_days_0#' => $txt['days'][0],
901
				'#txt_days_1#' => $txt['days'][1],
902
				'#txt_days_2#' => $txt['days'][2],
903
				'#txt_days_3#' => $txt['days'][3],
904
				'#txt_days_4#' => $txt['days'][4],
905
				'#txt_days_5#' => $txt['days'][5],
906
				'#txt_days_6#' => $txt['days'][6],
907
			));
908
909
		if (strpos($timestring, '#txt_months_short_') !== false)
910
			$timestring = strtr($timestring, array(
911
				'#txt_months_short_01#' => $txt['months_short'][1],
912
				'#txt_months_short_02#' => $txt['months_short'][2],
913
				'#txt_months_short_03#' => $txt['months_short'][3],
914
				'#txt_months_short_04#' => $txt['months_short'][4],
915
				'#txt_months_short_05#' => $txt['months_short'][5],
916
				'#txt_months_short_06#' => $txt['months_short'][6],
917
				'#txt_months_short_07#' => $txt['months_short'][7],
918
				'#txt_months_short_08#' => $txt['months_short'][8],
919
				'#txt_months_short_09#' => $txt['months_short'][9],
920
				'#txt_months_short_10#' => $txt['months_short'][10],
921
				'#txt_months_short_11#' => $txt['months_short'][11],
922
				'#txt_months_short_12#' => $txt['months_short'][12],
923
			));
924
925
		if (strpos($timestring, '#txt_months_') !== false)
926
			$timestring = strtr($timestring, array(
927
				'#txt_months_01#' => $txt['months'][1],
928
				'#txt_months_02#' => $txt['months'][2],
929
				'#txt_months_03#' => $txt['months'][3],
930
				'#txt_months_04#' => $txt['months'][4],
931
				'#txt_months_05#' => $txt['months'][5],
932
				'#txt_months_06#' => $txt['months'][6],
933
				'#txt_months_07#' => $txt['months'][7],
934
				'#txt_months_08#' => $txt['months'][8],
935
				'#txt_months_09#' => $txt['months'][9],
936
				'#txt_months_10#' => $txt['months'][10],
937
				'#txt_months_11#' => $txt['months'][11],
938
				'#txt_months_12#' => $txt['months'][12],
939
			));
940
	}
941
942
	// Restore any literal percent characters, add the prefix, and we're done.
943
	return $prefix . str_replace('&#37;', '%', $timestring);
944
}
945
946
/**
947
 * Gets a version of a strftime() format that only shows the date or time components
948
 *
949
 * @param string $type Either 'date' or 'time'.
950
 * @param string $format A strftime() format to process. Defaults to $user_info['time_format'].
951
 * @return string A strftime() format string
952
 */
953
function get_date_or_time_format($type = '', $format = '')
954
{
955
	global $user_info, $modSettings;
956
	static $formats;
957
958
	// If the format is invalid, fall back to defaults.
959
	if (strpos($format, '%') === false)
960
		$format = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %k:%M');
961
962
	$orig_format = $format;
963
964
	// Have we already done this?
965
	if (isset($formats[$orig_format][$type]))
966
		return $formats[$orig_format][$type];
967
968
	if ($type === 'date')
969
	{
970
		$specifications = array(
971
			// Day
972
			'%a' => '%a', '%A' => '%A', '%e' => '%e', '%d' => '%d', '%j' => '%j', '%u' => '%u', '%w' => '%w',
973
			// Week
974
			'%U' => '%U', '%V' => '%V', '%W' => '%W',
975
			// Month
976
			'%b' => '%b', '%B' => '%B', '%h' => '%h', '%m' => '%m',
977
			// Year
978
			'%C' => '%C', '%g' => '%g', '%G' => '%G', '%y' => '%y', '%Y' => '%Y',
979
			// Time
980
			'%H' => '', '%k' => '', '%I' => '', '%l' => '', '%M' => '', '%p' => '', '%P' => '',
981
			'%r' => '', '%R' => '', '%S' => '', '%T' => '', '%X' => '', '%z' => '', '%Z' => '',
982
			// Time and Date Stamps
983
			'%c' => '%x', '%D' => '%D', '%F' => '%F', '%s' => '%s', '%x' => '%x',
984
			// Miscellaneous
985
			'%n' => '', '%t' => '', '%%' => '%%',
986
		);
987
988
		$default_format = '%F';
989
	}
990
	elseif ($type === 'time')
991
	{
992
		$specifications = array(
993
			// Day
994
			'%a' => '', '%A' => '', '%e' => '', '%d' => '', '%j' => '', '%u' => '', '%w' => '',
995
			// Week
996
			'%U' => '', '%V' => '', '%W' => '',
997
			// Month
998
			'%b' => '', '%B' => '', '%h' => '', '%m' => '',
999
			// Year
1000
			'%C' => '', '%g' => '', '%G' => '', '%y' => '', '%Y' => '',
1001
			// Time
1002
			'%H' => '%H', '%k' => '%k', '%I' => '%I', '%l' => '%l', '%M' => '%M', '%p' => '%p', '%P' => '%P',
1003
			'%r' => '%r', '%R' => '%R', '%S' => '%S', '%T' => '%T', '%X' => '%X', '%z' => '%z', '%Z' => '%Z',
1004
			// Time and Date Stamps
1005
			'%c' => '%X', '%D' => '', '%F' => '', '%s' => '%s', '%x' => '',
1006
			// Miscellaneous
1007
			'%n' => '', '%t' => '', '%%' => '%%',
1008
		);
1009
1010
		$default_format = '%k:%M';
1011
	}
1012
	// Invalid type requests just get the full format string.
1013
	else
1014
		return $format;
1015
1016
	// Separate the specifications we want from the ones we don't.
1017
	$wanted = array_filter($specifications);
1018
	$unwanted = array_diff(array_keys($specifications), $wanted);
1019
1020
	// First, make any necessary substitutions in the format.
1021
	$format = strtr($format, $wanted);
1022
1023
	// Next, strip out any specifications and literal text that we don't want.
1024
	$format_parts = preg_split('~%[' . (strtr(implode('', $unwanted), array('%' => ''))) . ']~u', $format);
1025
1026
	foreach ($format_parts as $p => $f)
1027
	{
1028
		if (strpos($f, '%') === false)
1029
			unset($format_parts[$p]);
1030
	}
1031
1032
	$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

1032
	$format = implode('', /** @scrutinizer ignore-type */ $format_parts);
Loading history...
1033
1034
	// Finally, strip out any unwanted leftovers.
1035
	// 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
1036
	$format = preg_replace(
1037
		array(
1038
			// Anything that isn't a specification, punctuation mark, or whitespace.
1039
			'~(?<!%)\p{L}|[^\p{L}\p{P}\s]~u',
1040
			// A series of punctuation marks (except %), possibly separated by whitespace.
1041
			'~([^%\P{P}])(\s*)(?' . '>(\1|[^%\P{Po}])\s*(?!$))*~u',
1042
			// Unwanted trailing punctuation and whitespace.
1043
			'~(?' . '>([\p{Pd}\p{Ps}\p{Pi}\p{Pc}]|[^%\P{Po}])\s*)*$~u',
1044
			// Unwanted opening punctuation and whitespace.
1045
			'~^\s*(?' . '>([\p{Pd}\p{Pe}\p{Pf}\p{Pc}]|[^%\P{Po}])\s*)*~u',
1046
		),
1047
		array(
1048
			'',
1049
			'$1$2',
1050
			'',
1051
			'',
1052
		),
1053
		$format
1054
	);
1055
1056
	// Gotta have something...
1057
	if (empty($format))
1058
		$format = $default_format;
1059
1060
	// Remember what we've done.
1061
	$formats[$orig_format][$type] = trim($format);
1062
1063
	return $formats[$orig_format][$type];
1064
}
1065
1066
/**
1067
 * Replaces special entities in strings with the real characters.
1068
 *
1069
 * Functionally equivalent to htmlspecialchars_decode(), except that this also
1070
 * replaces '&nbsp;' with a simple space character.
1071
 *
1072
 * @param string $string A string
1073
 * @return string The string without entities
1074
 */
1075
function un_htmlspecialchars($string)
1076
{
1077
	global $context;
1078
	static $translation = array();
1079
1080
	// Determine the character set... Default to UTF-8
1081
	if (empty($context['character_set']))
1082
		$charset = 'UTF-8';
1083
	// Use ISO-8859-1 in place of non-supported ISO-8859 charsets...
1084
	elseif (strpos($context['character_set'], 'ISO-8859-') !== false && !in_array($context['character_set'], array('ISO-8859-5', 'ISO-8859-15')))
1085
		$charset = 'ISO-8859-1';
1086
	else
1087
		$charset = $context['character_set'];
1088
1089
	if (empty($translation))
1090
		$translation = array_flip(get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES, $charset)) + array('&#039;' => '\'', '&#39;' => '\'', '&nbsp;' => ' ');
1091
1092
	return strtr($string, $translation);
1093
}
1094
1095
/**
1096
 * Shorten a subject + internationalization concerns.
1097
 *
1098
 * - shortens a subject so that it is either shorter than length, or that length plus an ellipsis.
1099
 * - respects internationalization characters and entities as one character.
1100
 * - avoids trailing entities.
1101
 * - returns the shortened string.
1102
 *
1103
 * @param string $subject The subject
1104
 * @param int $len How many characters to limit it to
1105
 * @return string The shortened subject - either the entire subject (if it's <= $len) or the subject shortened to $len characters with "..." appended
1106
 */
1107
function shorten_subject($subject, $len)
1108
{
1109
	global $smcFunc;
1110
1111
	// It was already short enough!
1112
	if ($smcFunc['strlen']($subject) <= $len)
1113
		return $subject;
1114
1115
	// Shorten it by the length it was too long, and strip off junk from the end.
1116
	return $smcFunc['substr']($subject, 0, $len) . '...';
1117
}
1118
1119
/**
1120
 * Gets the current time with offset.
1121
 *
1122
 * - always applies the offset in the time_offset setting.
1123
 *
1124
 * @param bool $use_user_offset Whether to apply the user's offset as well
1125
 * @param int $timestamp A timestamp (null to use current time)
1126
 * @return int Seconds since the unix epoch, with forum time offset and (optionally) user time offset applied
1127
 */
1128
function forum_time($use_user_offset = true, $timestamp = null)
1129
{
1130
	global $user_info, $modSettings;
1131
1132
	if ($timestamp === null)
1133
		$timestamp = time();
1134
	elseif ($timestamp == 0)
1135
		return 0;
1136
1137
	return $timestamp + ($modSettings['time_offset'] + ($use_user_offset ? $user_info['time_offset'] : 0)) * 3600;
1138
}
1139
1140
/**
1141
 * Calculates all the possible permutations (orders) of array.
1142
 * should not be called on huge arrays (bigger than like 10 elements.)
1143
 * returns an array containing each permutation.
1144
 *
1145
 * @deprecated since 2.1
1146
 * @param array $array An array
1147
 * @return array An array containing each permutation
1148
 */
1149
function permute($array)
1150
{
1151
	$orders = array($array);
1152
1153
	$n = count($array);
1154
	$p = range(0, $n);
1155
	for ($i = 1; $i < $n; null)
1156
	{
1157
		$p[$i]--;
1158
		$j = $i % 2 != 0 ? $p[$i] : 0;
1159
1160
		$temp = $array[$i];
1161
		$array[$i] = $array[$j];
1162
		$array[$j] = $temp;
1163
1164
		for ($i = 1; $p[$i] == 0; $i++)
1165
			$p[$i] = 1;
1166
1167
		$orders[] = $array;
1168
	}
1169
1170
	return $orders;
1171
}
1172
1173
/**
1174
 * Parse bulletin board code in a string, as well as smileys optionally.
1175
 *
1176
 * - only parses bbc tags which are not disabled in disabledBBC.
1177
 * - handles basic HTML, if enablePostHTML is on.
1178
 * - caches the from/to replace regular expressions so as not to reload them every time a string is parsed.
1179
 * - only parses smileys if smileys is true.
1180
 * - does nothing if the enableBBC setting is off.
1181
 * - uses the cache_id as a unique identifier to facilitate any caching it may do.
1182
 * - returns the modified message.
1183
 *
1184
 * @param string|bool $message The message.
1185
 *		When a empty string, nothing is done.
1186
 *		When false we provide a list of BBC codes available.
1187
 *		When a string, the message is parsed and bbc handled.
1188
 * @param bool $smileys Whether to parse smileys as well
1189
 * @param string $cache_id The cache ID
1190
 * @param array $parse_tags If set, only parses these tags rather than all of them
1191
 * @return string The parsed message
1192
 */
1193
function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = array())
1194
{
1195
	global $smcFunc, $txt, $scripturl, $context, $modSettings, $user_info, $sourcedir, $cache_enable;
1196
	static $bbc_lang_locales = array(), $itemcodes = array(), $no_autolink_tags = array();
1197
	static $disabled, $alltags_regex = '', $param_regexes = array();
1198
1199
	// Don't waste cycles
1200
	if ($message === '')
1201
		return '';
1202
1203
	// Just in case it wasn't determined yet whether UTF-8 is enabled.
1204
	if (!isset($context['utf8']))
1205
		$context['utf8'] = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8';
1206
1207
	// Clean up any cut/paste issues we may have
1208
	$message = sanitizeMSCutPaste($message);
1209
1210
	// If the load average is too high, don't parse the BBC.
1211
	if (!empty($context['load_average']) && !empty($modSettings['bbc']) && $context['load_average'] >= $modSettings['bbc'])
1212
	{
1213
		$context['disabled_parse_bbc'] = true;
1214
		return $message;
1215
	}
1216
1217
	if ($smileys !== null && ($smileys == '1' || $smileys == '0'))
1218
		$smileys = (bool) $smileys;
1219
1220
	if (empty($modSettings['enableBBC']) && $message !== false)
1221
	{
1222
		if ($smileys === true)
1223
			parsesmileys($message);
1224
1225
		return $message;
1226
	}
1227
1228
	// If we already have a version of the BBCodes for the current language, use that. Otherwise, make one.
1229
	if (!empty($bbc_lang_locales[$txt['lang_locale']]))
1230
		$bbc_codes = $bbc_lang_locales[$txt['lang_locale']];
1231
	else
1232
		$bbc_codes = array();
1233
1234
	// If we are not doing every tag then we don't cache this run.
1235
	if (!empty($parse_tags))
1236
		$bbc_codes = array();
1237
1238
	// Ensure $modSettings['tld_regex'] contains a valid regex for the autolinker
1239
	if (!empty($modSettings['autoLinkUrls']))
1240
		set_tld_regex();
1241
1242
	// Allow mods access before entering the main parse_bbc loop
1243
	call_integration_hook('integrate_pre_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
1244
1245
	// Sift out the bbc for a performance improvement.
1246
	if (empty($bbc_codes) || $message === false || !empty($parse_tags))
1247
	{
1248
		if (!empty($modSettings['disabledBBC']))
1249
		{
1250
			$disabled = array();
1251
1252
			$temp = explode(',', strtolower($modSettings['disabledBBC']));
1253
1254
			foreach ($temp as $tag)
1255
				$disabled[trim($tag)] = true;
1256
1257
			if (in_array('color', $disabled))
1258
				$disabled = array_merge($disabled, array(
1259
					'black' => true,
1260
					'white' => true,
1261
					'red' => true,
1262
					'green' => true,
1263
					'blue' => true,
1264
					)
1265
				);
1266
		}
1267
1268
		// The YouTube bbc needs this for its origin parameter
1269
		$scripturl_parts = parse_url($scripturl);
1270
		$hosturl = $scripturl_parts['scheme'] . '://' . $scripturl_parts['host'];
1271
1272
		/* The following bbc are formatted as an array, with keys as follows:
1273
1274
			tag: the tag's name - should be lowercase!
1275
1276
			type: one of...
1277
				- (missing): [tag]parsed content[/tag]
1278
				- unparsed_equals: [tag=xyz]parsed content[/tag]
1279
				- parsed_equals: [tag=parsed data]parsed content[/tag]
1280
				- unparsed_content: [tag]unparsed content[/tag]
1281
				- closed: [tag], [tag/], [tag /]
1282
				- unparsed_commas: [tag=1,2,3]parsed content[/tag]
1283
				- unparsed_commas_content: [tag=1,2,3]unparsed content[/tag]
1284
				- unparsed_equals_content: [tag=...]unparsed content[/tag]
1285
1286
			parameters: an optional array of parameters, for the form
1287
			  [tag abc=123]content[/tag].  The array is an associative array
1288
			  where the keys are the parameter names, and the values are an
1289
			  array which may contain the following:
1290
				- match: a regular expression to validate and match the value.
1291
				- quoted: true if the value should be quoted.
1292
				- validate: callback to evaluate on the data, which is $data.
1293
				- value: a string in which to replace $1 with the data.
1294
					Either value or validate may be used, not both.
1295
				- optional: true if the parameter is optional.
1296
				- default: a default value for missing optional parameters.
1297
1298
			test: a regular expression to test immediately after the tag's
1299
			  '=', ' ' or ']'.  Typically, should have a \] at the end.
1300
			  Optional.
1301
1302
			content: only available for unparsed_content, closed,
1303
			  unparsed_commas_content, and unparsed_equals_content.
1304
			  $1 is replaced with the content of the tag.  Parameters
1305
			  are replaced in the form {param}.  For unparsed_commas_content,
1306
			  $2, $3, ..., $n are replaced.
1307
1308
			before: only when content is not used, to go before any
1309
			  content.  For unparsed_equals, $1 is replaced with the value.
1310
			  For unparsed_commas, $1, $2, ..., $n are replaced.
1311
1312
			after: similar to before in every way, except that it is used
1313
			  when the tag is closed.
1314
1315
			disabled_content: used in place of content when the tag is
1316
			  disabled.  For closed, default is '', otherwise it is '$1' if
1317
			  block_level is false, '<div>$1</div>' elsewise.
1318
1319
			disabled_before: used in place of before when disabled.  Defaults
1320
			  to '<div>' if block_level, '' if not.
1321
1322
			disabled_after: used in place of after when disabled.  Defaults
1323
			  to '</div>' if block_level, '' if not.
1324
1325
			block_level: set to true the tag is a "block level" tag, similar
1326
			  to HTML.  Block level tags cannot be nested inside tags that are
1327
			  not block level, and will not be implicitly closed as easily.
1328
			  One break following a block level tag may also be removed.
1329
1330
			trim: if set, and 'inside' whitespace after the begin tag will be
1331
			  removed.  If set to 'outside', whitespace after the end tag will
1332
			  meet the same fate.
1333
1334
			validate: except when type is missing or 'closed', a callback to
1335
			  validate the data as $data.  Depending on the tag's type, $data
1336
			  may be a string or an array of strings (corresponding to the
1337
			  replacement.)
1338
1339
			quoted: when type is 'unparsed_equals' or 'parsed_equals' only,
1340
			  may be not set, 'optional', or 'required' corresponding to if
1341
			  the content may be quoted.  This allows the parser to read
1342
			  [tag="abc]def[esdf]"] properly.
1343
1344
			require_parents: an array of tag names, or not set.  If set, the
1345
			  enclosing tag *must* be one of the listed tags, or parsing won't
1346
			  occur.
1347
1348
			require_children: similar to require_parents, if set children
1349
			  won't be parsed if they are not in the list.
1350
1351
			disallow_children: similar to, but very different from,
1352
			  require_children, if it is set the listed tags will not be
1353
			  parsed inside the tag.
1354
1355
			parsed_tags_allowed: an array restricting what BBC can be in the
1356
			  parsed_equals parameter, if desired.
1357
		*/
1358
1359
		$codes = array(
1360
			array(
1361
				'tag' => 'abbr',
1362
				'type' => 'unparsed_equals',
1363
				'before' => '<abbr title="$1">',
1364
				'after' => '</abbr>',
1365
				'quoted' => 'optional',
1366
				'disabled_after' => ' ($1)',
1367
			),
1368
			// Legacy (and just an alias for [abbr] even when enabled)
1369
			array(
1370
				'tag' => 'acronym',
1371
				'type' => 'unparsed_equals',
1372
				'before' => '<abbr title="$1">',
1373
				'after' => '</abbr>',
1374
				'quoted' => 'optional',
1375
				'disabled_after' => ' ($1)',
1376
			),
1377
			array(
1378
				'tag' => 'anchor',
1379
				'type' => 'unparsed_equals',
1380
				'test' => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]',
1381
				'before' => '<span id="post_$1">',
1382
				'after' => '</span>',
1383
			),
1384
			array(
1385
				'tag' => 'attach',
1386
				'type' => 'unparsed_content',
1387
				'parameters' => array(
1388
					'id' => array('match' => '(\d+)'),
1389
					'alt' => array('optional' => true),
1390
					'width' => array('optional' => true, 'match' => '(\d+)'),
1391
					'height' => array('optional' => true, 'match' => '(\d+)'),
1392
					'display' => array('optional' => true, 'match' => '(link|embed)'),
1393
				),
1394
				'content' => '$1',
1395
				'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...
1396
				{
1397
					$returnContext = '';
1398
1399
					// BBC or the entire attachments feature is disabled
1400
					if (empty($modSettings['attachmentEnable']) || !empty($disabled['attach']))
1401
						return $data;
1402
1403
					// Save the attach ID.
1404
					$attachID = $params['{id}'];
1405
1406
					// Kinda need this.
1407
					require_once($sourcedir . '/Subs-Attachments.php');
1408
1409
					$currentAttachment = parseAttachBBC($attachID);
1410
1411
					// parseAttachBBC will return a string ($txt key) rather than dying with a fatal_error. Up to you to decide what to do.
1412
					if (is_string($currentAttachment))
1413
						return $data = !empty($txt[$currentAttachment]) ? $txt[$currentAttachment] : $currentAttachment;
1414
1415
					// We need a display mode.
1416
					if (empty($params['{display}']))
1417
					{
1418
						// Images, video, and audio are embedded by default.
1419
						if (!empty($currentAttachment['is_image']) || strpos($currentAttachment['mime_type'], 'video/') === 0 || strpos($currentAttachment['mime_type'], 'audio/') === 0)
1420
							$params['{display}'] = 'embed';
1421
						// Anything else shows a link by default.
1422
						else
1423
							$params['{display}'] = 'link';
1424
					}
1425
1426
					// Embedded file.
1427
					if ($params['{display}'] == 'embed')
1428
					{
1429
						$alt = ' alt="' . (!empty($params['{alt}']) ? $params['{alt}'] : $currentAttachment['name']) . '"';
1430
						$title = !empty($data) ? ' title="' . $smcFunc['htmlspecialchars']($data) . '"' : '';
1431
1432
						$width = !empty($params['{width}']) ? $params['{width}'] : (!empty($currentAttachment['width']) ? $currentAttachment['width'] : '');
1433
						$height = !empty($params['{height}']) ? $params['{height}'] : (!empty($currentAttachment['height']) ? $currentAttachment['height'] : '');
1434
1435
						// Image.
1436
						if (!empty($currentAttachment['is_image']))
1437
						{
1438
							$width = !empty($width) ? ' width="' . $width . '"' : '';
1439
							$height = !empty($height) ? ' height="' . $height . '"' : '';
1440
1441
							if ($currentAttachment['thumbnail']['has_thumb'] && empty($params['{width}']) && empty($params['{height}']))
1442
								$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>';
1443
							else
1444
								$returnContext .= '<img src="' . $currentAttachment['href'] . ';image"' . $alt . $title . $width . $height . ' class="bbc_img"/>';
1445
						}
1446
						// Video.
1447
						elseif (strpos($currentAttachment['mime_type'], 'video/') === 0)
1448
						{
1449
							$width = !empty($width) ? ' width="' . $width . '"' : '';
1450
							$height = !empty($height) ? ' height="' . $height . '"' : '';
1451
1452
							$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>' : '');
1453
						}
1454
						// Audio.
1455
						elseif (strpos($currentAttachment['mime_type'], 'audio/') === 0)
1456
						{
1457
							$width = 'max-width:100%; width: ' . (!empty($width) ? $width : '400') . 'px;';
1458
							$height = !empty($height) ? 'height: ' . $height . 'px;' : '';
1459
1460
							$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>';
1461
						}
1462
						// Anything else.
1463
						else
1464
						{
1465
							$width = !empty($width) ? ' width="' . $width . '"' : '';
1466
							$height = !empty($height) ? ' height="' . $height . '"' : '';
1467
1468
							$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>';
1469
						}
1470
					}
1471
1472
					// No image. Show a link.
1473
					else
1474
						$returnContext .= '<a href="' . $currentAttachment['href'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . '</a>';
1475
1476
					// Gotta append what we just did.
1477
					$data = $returnContext;
1478
				},
1479
			),
1480
			array(
1481
				'tag' => 'b',
1482
				'before' => '<b>',
1483
				'after' => '</b>',
1484
			),
1485
			// Legacy (equivalent to [ltr] or [rtl])
1486
			array(
1487
				'tag' => 'bdo',
1488
				'type' => 'unparsed_equals',
1489
				'before' => '<bdo dir="$1">',
1490
				'after' => '</bdo>',
1491
				'test' => '(rtl|ltr)\]',
1492
				'block_level' => true,
1493
			),
1494
			// Legacy (alias of [color=black])
1495
			array(
1496
				'tag' => 'black',
1497
				'before' => '<span style="color: black;" class="bbc_color">',
1498
				'after' => '</span>',
1499
			),
1500
			// Legacy (alias of [color=blue])
1501
			array(
1502
				'tag' => 'blue',
1503
				'before' => '<span style="color: blue;" class="bbc_color">',
1504
				'after' => '</span>',
1505
			),
1506
			array(
1507
				'tag' => 'br',
1508
				'type' => 'closed',
1509
				'content' => '<br>',
1510
			),
1511
			array(
1512
				'tag' => 'center',
1513
				'before' => '<div class="centertext">',
1514
				'after' => '</div>',
1515
				'block_level' => true,
1516
			),
1517
			array(
1518
				'tag' => 'code',
1519
				'type' => 'unparsed_content',
1520
				'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>',
1521
				// @todo Maybe this can be simplified?
1522
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1523
				{
1524
					if (!isset($disabled['code']))
1525
					{
1526
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
1527
1528
						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

1528
						for ($php_i = 0, $php_n = count(/** @scrutinizer ignore-type */ $php_parts); $php_i < $php_n; $php_i++)
Loading history...
1529
						{
1530
							// Do PHP code coloring?
1531
							if ($php_parts[$php_i] != '&lt;?php')
1532
								continue;
1533
1534
							$php_string = '';
1535
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1536
							{
1537
								$php_string .= $php_parts[$php_i];
1538
								$php_parts[$php_i++] = '';
1539
							}
1540
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1541
						}
1542
1543
						// Fix the PHP code stuff...
1544
						$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

1544
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', /** @scrutinizer ignore-type */ $php_parts));
Loading history...
1545
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1546
1547
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1548
						if (!empty($context['browser']['is_opera']))
1549
							$data .= '&nbsp;';
1550
					}
1551
				},
1552
				'block_level' => true,
1553
			),
1554
			array(
1555
				'tag' => 'code',
1556
				'type' => 'unparsed_equals_content',
1557
				'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>',
1558
				// @todo Maybe this can be simplified?
1559
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1560
				{
1561
					if (!isset($disabled['code']))
1562
					{
1563
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1564
1565
						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

1565
						for ($php_i = 0, $php_n = count(/** @scrutinizer ignore-type */ $php_parts); $php_i < $php_n; $php_i++)
Loading history...
1566
						{
1567
							// Do PHP code coloring?
1568
							if ($php_parts[$php_i] != '&lt;?php')
1569
								continue;
1570
1571
							$php_string = '';
1572
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1573
							{
1574
								$php_string .= $php_parts[$php_i];
1575
								$php_parts[$php_i++] = '';
1576
							}
1577
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1578
						}
1579
1580
						// Fix the PHP code stuff...
1581
						$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

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

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

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

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

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

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

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

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

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

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

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

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

5537
			$fetch_data->get_url_data($url, /** @scrutinizer ignore-type */ $post_data);
Loading history...
5538
5539
			// no errors and a 200 result, then we have a good dataset, well we at least have data. ;)
5540
			if ($fetch_data->result('code') == 200 && !$fetch_data->result('error'))
5541
				$data = $fetch_data->result('body');
5542
			else
5543
				return false;
5544
		}
5545
5546
		// Neither fsockopen nor curl are available. Well, phooey.
5547
		else
5548
			return false;
5549
	}
5550
	else
5551
	{
5552
		// Umm, this shouldn't happen?
5553
		trigger_error('fetch_web_data(): Bad URL', E_USER_NOTICE);
5554
		$data = false;
5555
	}
5556
5557
	return $data;
5558
}
5559
5560
/**
5561
 * Prepares an array of "likes" info for the topic specified by $topic
5562
 *
5563
 * @param integer $topic The topic ID to fetch the info from.
5564
 * @return array An array of IDs of messages in the specified topic that the current user likes
5565
 */
5566
function prepareLikesContext($topic)
5567
{
5568
	global $user_info, $smcFunc;
5569
5570
	// Make sure we have something to work with.
5571
	if (empty($topic))
5572
		return array();
5573
5574
	// We already know the number of likes per message, we just want to know whether the current user liked it or not.
5575
	$user = $user_info['id'];
5576
	$cache_key = 'likes_topic_' . $topic . '_' . $user;
5577
	$ttl = 180;
5578
5579
	if (($temp = cache_get_data($cache_key, $ttl)) === null)
5580
	{
5581
		$temp = array();
5582
		$request = $smcFunc['db_query']('', '
5583
			SELECT content_id
5584
			FROM {db_prefix}user_likes AS l
5585
				INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg)
5586
			WHERE l.id_member = {int:current_user}
5587
				AND l.content_type = {literal:msg}
5588
				AND m.id_topic = {int:topic}',
5589
			array(
5590
				'current_user' => $user,
5591
				'topic' => $topic,
5592
			)
5593
		);
5594
		while ($row = $smcFunc['db_fetch_assoc']($request))
5595
			$temp[] = (int) $row['content_id'];
5596
5597
		cache_put_data($cache_key, $temp, $ttl);
5598
	}
5599
5600
	return $temp;
5601
}
5602
5603
/**
5604
 * Microsoft uses their own character set Code Page 1252 (CP1252), which is a
5605
 * superset of ISO 8859-1, defining several characters between DEC 128 and 159
5606
 * that are not normally displayable.  This converts the popular ones that
5607
 * appear from a cut and paste from windows.
5608
 *
5609
 * @param string $string The string
5610
 * @return string The sanitized string
5611
 */
5612
function sanitizeMSCutPaste($string)
5613
{
5614
	global $context;
5615
5616
	if (empty($string))
5617
		return $string;
5618
5619
	// UTF-8 occurences of MS special characters
5620
	$findchars_utf8 = array(
5621
		"\xe2\x80\x9a",	// single low-9 quotation mark
5622
		"\xe2\x80\x9e",	// double low-9 quotation mark
5623
		"\xe2\x80\xa6",	// horizontal ellipsis
5624
		"\xe2\x80\x98",	// left single curly quote
5625
		"\xe2\x80\x99",	// right single curly quote
5626
		"\xe2\x80\x9c",	// left double curly quote
5627
		"\xe2\x80\x9d",	// right double curly quote
5628
	);
5629
5630
	// windows 1252 / iso equivalents
5631
	$findchars_iso = array(
5632
		chr(130),
5633
		chr(132),
5634
		chr(133),
5635
		chr(145),
5636
		chr(146),
5637
		chr(147),
5638
		chr(148),
5639
	);
5640
5641
	// safe replacements
5642
	$replacechars = array(
5643
		',',	// &sbquo;
5644
		',,',	// &bdquo;
5645
		'...',	// &hellip;
5646
		"'",	// &lsquo;
5647
		"'",	// &rsquo;
5648
		'"',	// &ldquo;
5649
		'"',	// &rdquo;
5650
	);
5651
5652
	if ($context['utf8'])
5653
		$string = str_replace($findchars_utf8, $replacechars, $string);
5654
	else
5655
		$string = str_replace($findchars_iso, $replacechars, $string);
5656
5657
	return $string;
5658
}
5659
5660
/**
5661
 * Decode numeric html entities to their ascii or UTF8 equivalent character.
5662
 *
5663
 * Callback function for preg_replace_callback in subs-members
5664
 * Uses capture group 2 in the supplied array
5665
 * Does basic scan to ensure characters are inside a valid range
5666
 *
5667
 * @param array $matches An array of matches (relevant info should be the 3rd item)
5668
 * @return string A fixed string
5669
 */
5670
function replaceEntities__callback($matches)
5671
{
5672
	global $context;
5673
5674
	if (!isset($matches[2]))
5675
		return '';
5676
5677
	$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

5677
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5678
5679
	// remove left to right / right to left overrides
5680
	if ($num === 0x202D || $num === 0x202E)
5681
		return '';
5682
5683
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
5684
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
5685
		return '&#' . $num . ';';
5686
5687
	if (empty($context['utf8']))
5688
	{
5689
		// no control characters
5690
		if ($num < 0x20)
5691
			return '';
5692
		// text is text
5693
		elseif ($num < 0x80)
5694
			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

5694
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5695
		// all others get html-ised
5696
		else
5697
			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

5697
			return '&#' . /** @scrutinizer ignore-type */ $matches[2] . ';';
Loading history...
5698
	}
5699
	else
5700
	{
5701
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
5702
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
5703
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
5704
			return '';
5705
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
5706
		elseif ($num < 0x80)
5707
			return chr($num);
5708
		// <0x800 (2048)
5709
		elseif ($num < 0x800)
5710
			return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5711
		// < 0x10000 (65536)
5712
		elseif ($num < 0x10000)
5713
			return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5714
		// <= 0x10FFFF (1114111)
5715
		else
5716
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5717
	}
5718
}
5719
5720
/**
5721
 * Converts html entities to utf8 equivalents
5722
 *
5723
 * Callback function for preg_replace_callback
5724
 * Uses capture group 1 in the supplied array
5725
 * Does basic checks to keep characters inside a viewable range.
5726
 *
5727
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
5728
 * @return string The fixed string
5729
 */
5730
function fixchar__callback($matches)
5731
{
5732
	if (!isset($matches[1]))
5733
		return '';
5734
5735
	$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

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

5743
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5744
	// <0x800 (2048)
5745
	elseif ($num < 0x800)
5746
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5747
	// < 0x10000 (65536)
5748
	elseif ($num < 0x10000)
5749
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5750
	// <= 0x10FFFF (1114111)
5751
	else
5752
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5753
}
5754
5755
/**
5756
 * Strips out invalid html entities, replaces others with html style &#123; codes
5757
 *
5758
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
5759
 * strpos, strlen, substr etc
5760
 *
5761
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
5762
 * @return string The fixed string
5763
 */
5764
function entity_fix__callback($matches)
5765
{
5766
	if (!isset($matches[2]))
5767
		return '';
5768
5769
	$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

5769
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5770
5771
	// we don't allow control characters, characters out of range, byte markers, etc
5772
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
5773
		return '';
5774
	else
5775
		return '&#' . $num . ';';
5776
}
5777
5778
/**
5779
 * Return a Gravatar URL based on
5780
 * - the supplied email address,
5781
 * - the global maximum rating,
5782
 * - the global default fallback,
5783
 * - maximum sizes as set in the admin panel.
5784
 *
5785
 * It is SSL aware, and caches most of the parameters.
5786
 *
5787
 * @param string $email_address The user's email address
5788
 * @return string The gravatar URL
5789
 */
5790
function get_gravatar_url($email_address)
5791
{
5792
	global $modSettings, $smcFunc;
5793
	static $url_params = null;
5794
5795
	if ($url_params === null)
5796
	{
5797
		$ratings = array('G', 'PG', 'R', 'X');
5798
		$defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank');
5799
		$url_params = array();
5800
		if (!empty($modSettings['gravatarMaxRating']) && in_array($modSettings['gravatarMaxRating'], $ratings))
5801
			$url_params[] = 'rating=' . $modSettings['gravatarMaxRating'];
5802
		if (!empty($modSettings['gravatarDefault']) && in_array($modSettings['gravatarDefault'], $defaults))
5803
			$url_params[] = 'default=' . $modSettings['gravatarDefault'];
5804
		if (!empty($modSettings['avatar_max_width_external']))
5805
		{
5806
			$size_string = (int) $modSettings['avatar_max_width_external'];
5807
			if (!empty($modSettings['avatar_max_height_external']))
5808
				$size_string = min($size_string, $modSettings['avatar_max_height_external']);
5809
		}
5810
5811
		if (!empty($size_string))
5812
			$url_params[] = 's=' . $size_string;
5813
	}
5814
	$http_method = !empty($modSettings['force_ssl']) ? 'https://secure' : 'http://www';
5815
5816
	return $http_method . '.gravatar.com/avatar/' . md5($smcFunc['strtolower']($email_address)) . '?' . implode('&', $url_params);
5817
}
5818
5819
/**
5820
 * Get a list of timezones.
5821
 *
5822
 * @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'.
5823
 * @return array An array of timezone info.
5824
 */
5825
function smf_list_timezones($when = 'now')
5826
{
5827
	global $smcFunc, $modSettings, $tztxt, $txt, $cur_profile;
5828
	static $timezones = null, $lastwhen = null;
5829
5830
	// No point doing this over if we already did it once
5831
	if (!empty($timezones) && $when == $lastwhen)
5832
		return $timezones;
5833
	else
5834
		$lastwhen = $when;
5835
5836
	// Parseable datetime string?
5837
	if (is_int($timestamp = strtotime($when)))
0 ignored issues
show
introduced by
The condition is_int($timestamp = strtotime($when)) is always true.
Loading history...
5838
		$when = $timestamp;
5839
5840
	// A Unix timestamp?
5841
	elseif (is_numeric($when))
5842
		$when = intval($when);
5843
5844
	// Invalid value? Just get current Unix timestamp.
5845
	else
5846
		$when = time();
5847
5848
	// We'll need these too
5849
	$date_when = date_create('@' . $when);
5850
	$later = strtotime('@' . $when . ' + 1 year');
5851
5852
	// Load up any custom time zone descriptions we might have
5853
	loadLanguage('Timezones');
5854
5855
	// Should we put time zones from certain countries at the top of the list?
5856
	$priority_countries = !empty($modSettings['timezone_priority_countries']) ? explode(',', $modSettings['timezone_priority_countries']) : array();
5857
	$priority_tzids = array();
5858
	foreach ($priority_countries as $country)
5859
	{
5860
		$country_tzids = @timezone_identifiers_list(DateTimeZone::PER_COUNTRY, strtoupper(trim($country)));
5861
		if (!empty($country_tzids))
5862
			$priority_tzids = array_merge($priority_tzids, $country_tzids);
5863
	}
5864
5865
	// Antarctic research stations should be listed last, unless you're running a penguin forum
5866
	$low_priority_tzids = !in_array('AQ', $priority_countries) ? timezone_identifiers_list(DateTimeZone::ANTARCTICA) : array();
5867
5868
	// Process the preferred timezones first, then the normal ones, then the low priority ones.
5869
	$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 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

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

5869
	$tzids = array_merge(array_keys($tztxt), array_diff(timezone_identifiers_list(), array_keys($tztxt), /** @scrutinizer ignore-type */ $low_priority_tzids), $low_priority_tzids);
Loading history...
Bug introduced by
It seems like $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

5869
	$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...
5870
5871
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
5872
	foreach ($tzids as $tzid)
5873
	{
5874
		// We don't want UTC right now
5875
		if ($tzid == 'UTC')
5876
			continue;
5877
5878
		$tz = timezone_open($tzid);
5879
5880
		// First, get the set of transition rules for this tzid
5881
		$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

5881
		$tzinfo = timezone_transitions_get(/** @scrutinizer ignore-type */ $tz, $when, $later);
Loading history...
5882
5883
		// Use the entire set of transition rules as the array *key* so we can avoid duplicates
5884
		$tzkey = serialize($tzinfo);
5885
5886
		// Next, get the geographic info for this tzid
5887
		$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

5887
		$tzgeo = timezone_location_get(/** @scrutinizer ignore-type */ $tz);
Loading history...
5888
5889
		// Don't overwrite our preferred tzids
5890
		if (empty($zones[$tzkey]['tzid']))
5891
		{
5892
			$zones[$tzkey]['tzid'] = $tzid;
5893
			$zones[$tzkey]['abbr'] = $tzinfo[0]['abbr'];
5894
		}
5895
5896
		// A time zone from a prioritized country?
5897
		if (in_array($tzid, $priority_tzids))
5898
			$priority_zones[$tzkey] = true;
5899
5900
		// Keep track of the location and offset for this tzid
5901
		if (!empty($txt[$tzid]))
5902
			$zones[$tzkey]['locations'][] = $txt[$tzid];
5903
		else
5904
		{
5905
			$tzid_parts = explode('/', $tzid);
5906
			$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
5907
		}
5908
		$offsets[$tzkey] = $tzinfo[0]['offset'];
5909
		$longitudes[$tzkey] = empty($longitudes[$tzkey]) ? $tzgeo['longitude'] : $longitudes[$tzkey];
5910
5911
		// Remember this for later
5912
		if (isset($cur_profile['timezone']) && $cur_profile['timezone'] == $tzid)
5913
			$member_tzkey = $tzkey;
5914
	}
5915
5916
	// Sort by offset then longitude
5917
	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 5872. 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 5872. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
5918
5919
	// Build the final array of formatted values
5920
	$priority_timezones = array();
5921
	$timezones = array();
5922
	foreach ($zones as $tzkey => $tzvalue)
5923
	{
5924
		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

5924
		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

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

6433
		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

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

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

7167
				$value = substr(/** @scrutinizer ignore-type */ $value, 0, $param_max - strlen($key) - 5);
Loading history...
Bug introduced by
$param_max - strlen($key) - 5 of type double is incompatible with the type integer 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

7167
				$value = substr($value, 0, /** @scrutinizer ignore-type */ $param_max - strlen($key) - 5);
Loading history...
7168
		}
7169
7170
		return $array;
7171
	}
7172
}
7173
7174
/**
7175
 * array_length Recursive
7176
 * @param $array
7177
 * @param int $deep How many levels should the function
7178
 * @return int
7179
 */
7180
function array_length($array, $deep = 3)
7181
{
7182
	// Work with arrays
7183
	$array = (array) $array;
7184
	$length = 0;
7185
7186
	$deep_count = $deep - 1;
7187
7188
	foreach ($array as $value)
7189
	{
7190
		// Recursive?
7191
		if (is_array($value))
7192
		{
7193
			// No can't do
7194
			if ($deep_count <= 0)
7195
				continue;
7196
7197
			$length += array_length($value, $deep_count);
7198
		}
7199
7200
		else
7201
			$length += strlen($value);
7202
	}
7203
7204
	return $length;
7205
}
7206
7207
?>