Passed
Pull Request — release-2.1 (#6344)
by Jon
04:59
created

smf_list_timezones()   F

Complexity

Conditions 48

Size

Total Lines 197
Code Lines 107

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 48
eloc 107
c 3
b 1
f 0
nop 1
dl 0
loc 197
rs 3.3333

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

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

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

1587
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', /** @scrutinizer ignore-type */ $php_parts));
Loading history...
1588
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1589
1590
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1591
						if (!empty($context['browser']['is_opera']))
1592
							$data .= '&nbsp;';
1593
					}
1594
				},
1595
				'block_level' => true,
1596
			),
1597
			array(
1598
				'tag' => 'code',
1599
				'type' => 'unparsed_equals_content',
1600
				'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>',
1601
				// @todo Maybe this can be simplified?
1602
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1603
				{
1604
					if (!isset($disabled['code']))
1605
					{
1606
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1607
1608
						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

1608
						for ($php_i = 0, $php_n = count(/** @scrutinizer ignore-type */ $php_parts); $php_i < $php_n; $php_i++)
Loading history...
1609
						{
1610
							// Do PHP code coloring?
1611
							if ($php_parts[$php_i] != '&lt;?php')
1612
								continue;
1613
1614
							$php_string = '';
1615
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1616
							{
1617
								$php_string .= $php_parts[$php_i];
1618
								$php_parts[$php_i++] = '';
1619
							}
1620
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1621
						}
1622
1623
						// Fix the PHP code stuff...
1624
						$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

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

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

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

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

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

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

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

2800
					sort(/** @scrutinizer ignore-type */ $given_params, SORT_STRING);
Loading history...
2801
2802
					$match = preg_match('~^' . $params_regexes[$regex_key] . '$~i', implode(' ', $given_params), $matches) !== 0;
2803
2804
					if ($match)
2805
						break;
2806
				}
2807
2808
				// Didn't match our parameter list, try the next possible.
2809
				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...
2810
					continue;
2811
2812
				$params = array();
2813
				for ($i = 1, $n = count($matches); $i < $n; $i += 2)
2814
				{
2815
					$key = strtok(ltrim($matches[$i]), '=');
2816
					if ($key === false)
2817
						continue;
2818
					elseif (isset($possible['parameters'][$key]['value']))
2819
						$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
2820
					elseif (isset($possible['parameters'][$key]['validate']))
2821
						$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
2822
					else
2823
						$params['{' . $key . '}'] = $matches[$i + 1];
2824
2825
					// Just to make sure: replace any $ or { so they can't interpolate wrongly.
2826
					$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
2827
				}
2828
2829
				foreach ($possible['parameters'] as $p => $info)
2830
				{
2831
					if (!isset($params['{' . $p . '}']))
2832
					{
2833
						if (!isset($info['default']))
2834
							$params['{' . $p . '}'] = '';
2835
						elseif (isset($possible['parameters'][$p]['value']))
2836
							$params['{' . $p . '}'] = strtr($possible['parameters'][$p]['value'], array('$1' => $info['default']));
2837
						elseif (isset($possible['parameters'][$p]['validate']))
2838
							$params['{' . $p . '}'] = $possible['parameters'][$p]['validate']($info['default']);
2839
						else
2840
							$params['{' . $p . '}'] = $info['default'];
2841
					}
2842
				}
2843
2844
				$tag = $possible;
2845
2846
				// Put the parameters into the string.
2847
				if (isset($tag['before']))
2848
					$tag['before'] = strtr($tag['before'], $params);
2849
				if (isset($tag['after']))
2850
					$tag['after'] = strtr($tag['after'], $params);
2851
				if (isset($tag['content']))
2852
					$tag['content'] = strtr($tag['content'], $params);
2853
2854
				$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...
2855
			}
2856
			else
2857
			{
2858
				$tag = $possible;
2859
				$params = array();
2860
			}
2861
			break;
2862
		}
2863
2864
		// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
2865
		if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
2866
		{
2867
			if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
2868
				continue;
2869
2870
			$tag = $itemcodes[$message[$pos + 1]];
2871
2872
			// First let's set up the tree: it needs to be in a list, or after an li.
2873
			if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
2874
			{
2875
				$open_tags[] = array(
2876
					'tag' => 'list',
2877
					'after' => '</ul>',
2878
					'block_level' => true,
2879
					'require_children' => array('li'),
2880
					'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2881
				);
2882
				$code = '<ul class="bbc_list">';
2883
			}
2884
			// We're in a list item already: another itemcode?  Close it first.
2885
			elseif ($inside['tag'] == 'li')
2886
			{
2887
				array_pop($open_tags);
2888
				$code = '</li>';
2889
			}
2890
			else
2891
				$code = '';
2892
2893
			// Now we open a new tag.
2894
			$open_tags[] = array(
2895
				'tag' => 'li',
2896
				'after' => '</li>',
2897
				'trim' => 'outside',
2898
				'block_level' => true,
2899
				'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2900
			);
2901
2902
			// First, open the tag...
2903
			$code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
2904
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
2905
			$pos += strlen($code) - 1 + 2;
2906
2907
			// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
2908
			$pos2 = strpos($message, '<br>', $pos);
2909
			$pos3 = strpos($message, '[/', $pos);
2910
			if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
2911
			{
2912
				preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 4), $matches);
2913
				$message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
2914
2915
				$open_tags[count($open_tags) - 2]['after'] = '</ul>';
2916
			}
2917
			// Tell the [list] that it needs to close specially.
2918
			else
2919
			{
2920
				// Move the li over, because we're not sure what we'll hit.
2921
				$open_tags[count($open_tags) - 1]['after'] = '';
2922
				$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
2923
			}
2924
2925
			continue;
2926
		}
2927
2928
		// Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
2929
		if ($tag === null && $inside !== null && !empty($inside['require_children']))
2930
		{
2931
			array_pop($open_tags);
2932
2933
			$message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
2934
			$pos += strlen($inside['after']) - 1 + 2;
2935
		}
2936
2937
		// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
2938
		if ($tag === null)
2939
			continue;
2940
2941
		// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
2942
		if (isset($inside['disallow_children']))
2943
			$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
2944
2945
		// Is this tag disabled?
2946
		if (isset($disabled[$tag['tag']]))
2947
		{
2948
			if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
2949
			{
2950
				$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
2951
				$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
2952
				$tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
2953
			}
2954
			elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
2955
			{
2956
				$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
2957
				$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
2958
			}
2959
			else
2960
				$tag['content'] = $tag['disabled_content'];
2961
		}
2962
2963
		// we use this a lot
2964
		$tag_strlen = strlen($tag['tag']);
2965
2966
		// The only special case is 'html', which doesn't need to close things.
2967
		if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
2968
		{
2969
			$n = count($open_tags) - 1;
2970
			while (empty($open_tags[$n]['block_level']) && $n >= 0)
2971
				$n--;
2972
2973
			// Close all the non block level tags so this tag isn't surrounded by them.
2974
			for ($i = count($open_tags) - 1; $i > $n; $i--)
2975
			{
2976
				$message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
2977
				$ot_strlen = strlen($open_tags[$i]['after']);
2978
				$pos += $ot_strlen + 2;
2979
				$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...
2980
2981
				// Trim or eat trailing stuff... see comment at the end of the big loop.
2982
				$whitespace_regex = '';
2983
				if (!empty($tag['block_level']))
2984
					$whitespace_regex .= '(&nbsp;|\s)*(<br>)?';
2985
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
2986
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2987
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
2988
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2989
2990
				array_pop($open_tags);
2991
			}
2992
		}
2993
2994
		// Can't read past the end of the message
2995
		$pos1 = min(strlen($message), $pos1);
2996
2997
		// No type means 'parsed_content'.
2998
		if (!isset($tag['type']))
2999
		{
3000
			$open_tags[] = $tag;
3001
			$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
3002
			$pos += strlen($tag['before']) - 1 + 2;
3003
		}
3004
		// Don't parse the content, just skip it.
3005
		elseif ($tag['type'] == 'unparsed_content')
3006
		{
3007
			$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
3008
			if ($pos2 === false)
3009
				continue;
3010
3011
			$data = substr($message, $pos1, $pos2 - $pos1);
3012
3013
			if (!empty($tag['block_level']) && substr($data, 0, 4) == '<br>')
3014
				$data = substr($data, 4);
3015
3016
			if (isset($tag['validate']))
3017
				$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...
3018
3019
			$code = strtr($tag['content'], array('$1' => $data));
3020
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
3021
3022
			$pos += strlen($code) - 1 + 2;
3023
			$last_pos = $pos + 1;
3024
		}
3025
		// Don't parse the content, just skip it.
3026
		elseif ($tag['type'] == 'unparsed_equals_content')
3027
		{
3028
			// The value may be quoted for some tags - check.
3029
			if (isset($tag['quoted']))
3030
			{
3031
				$quoted = substr($message, $pos1, 6) == '&quot;';
3032
				if ($tag['quoted'] != 'optional' && !$quoted)
3033
					continue;
3034
3035
				if ($quoted)
3036
					$pos1 += 6;
3037
			}
3038
			else
3039
				$quoted = false;
3040
3041
			$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...
3042
			if ($pos2 === false)
3043
				continue;
3044
3045
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3046
			if ($pos3 === false)
3047
				continue;
3048
3049
			$data = array(
3050
				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...
3051
				substr($message, $pos1, $pos2 - $pos1)
3052
			);
3053
3054
			if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '<br>')
3055
				$data[0] = substr($data[0], 4);
3056
3057
			// Validation for my parking, please!
3058
			if (isset($tag['validate']))
3059
				$tag['validate']($tag, $data, $disabled, $params);
3060
3061
			$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
3062
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3063
			$pos += strlen($code) - 1 + 2;
3064
		}
3065
		// A closed tag, with no content or value.
3066
		elseif ($tag['type'] == 'closed')
3067
		{
3068
			$pos2 = strpos($message, ']', $pos);
3069
			$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
3070
			$pos += strlen($tag['content']) - 1 + 2;
3071
		}
3072
		// This one is sorta ugly... :/.  Unfortunately, it's needed for flash.
3073
		elseif ($tag['type'] == 'unparsed_commas_content')
3074
		{
3075
			$pos2 = strpos($message, ']', $pos1);
3076
			if ($pos2 === false)
3077
				continue;
3078
3079
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3080
			if ($pos3 === false)
3081
				continue;
3082
3083
			// We want $1 to be the content, and the rest to be csv.
3084
			$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
3085
			$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
3086
3087
			if (isset($tag['validate']))
3088
				$tag['validate']($tag, $data, $disabled, $params);
3089
3090
			$code = $tag['content'];
3091
			foreach ($data as $k => $d)
3092
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3093
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3094
			$pos += strlen($code) - 1 + 2;
3095
		}
3096
		// This has parsed content, and a csv value which is unparsed.
3097
		elseif ($tag['type'] == 'unparsed_commas')
3098
		{
3099
			$pos2 = strpos($message, ']', $pos1);
3100
			if ($pos2 === false)
3101
				continue;
3102
3103
			$data = explode(',', substr($message, $pos1, $pos2 - $pos1));
3104
3105
			if (isset($tag['validate']))
3106
				$tag['validate']($tag, $data, $disabled, $params);
3107
3108
			// Fix after, for disabled code mainly.
3109
			foreach ($data as $k => $d)
3110
				$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
3111
3112
			$open_tags[] = $tag;
3113
3114
			// Replace them out, $1, $2, $3, $4, etc.
3115
			$code = $tag['before'];
3116
			foreach ($data as $k => $d)
3117
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3118
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1);
3119
			$pos += strlen($code) - 1 + 2;
3120
		}
3121
		// A tag set to a value, parsed or not.
3122
		elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
3123
		{
3124
			// The value may be quoted for some tags - check.
3125
			if (isset($tag['quoted']))
3126
			{
3127
				$quoted = substr($message, $pos1, 6) == '&quot;';
3128
				if ($tag['quoted'] != 'optional' && !$quoted)
3129
					continue;
3130
3131
				if ($quoted)
3132
					$pos1 += 6;
3133
			}
3134
			else
3135
				$quoted = false;
3136
3137
			if ($quoted)
3138
			{
3139
				$end_of_value = strpos($message, '&quot;]', $pos1);
3140
				$nested_tag = strpos($message, '=&quot;', $pos1);
3141
				if ($nested_tag && $nested_tag < $end_of_value)
3142
					// Nested tag with quoted value detected, use next end tag
3143
					$nested_tag_pos = strpos($message, $quoted == false ? ']' : '&quot;]', $pos1) + 6;
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

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

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

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

Loading history...
3147
			if ($pos2 === false)
3148
				continue;
3149
3150
			$data = substr($message, $pos1, $pos2 - $pos1);
3151
3152
			// Validation for my parking, please!
3153
			if (isset($tag['validate']))
3154
				$tag['validate']($tag, $data, $disabled, $params);
3155
3156
			// For parsed content, we must recurse to avoid security problems.
3157
			if ($tag['type'] != 'unparsed_equals')
3158
				$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
3159
3160
			$tag['after'] = strtr($tag['after'], array('$1' => $data));
3161
3162
			$open_tags[] = $tag;
3163
3164
			$code = strtr($tag['before'], array('$1' => $data));
3165
			$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...
3166
			$pos += strlen($code) - 1 + 2;
3167
		}
3168
3169
		// If this is block level, eat any breaks after it.
3170
		if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '<br>')
3171
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 5);
3172
3173
		// Are we trimming outside this tag?
3174
		if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
3175
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
3176
	}
3177
3178
	// Close any remaining tags.
3179
	while ($tag = array_pop($open_tags))
3180
		$message .= "\n" . $tag['after'] . "\n";
3181
3182
	// Parse the smileys within the parts where it can be done safely.
3183
	if ($smileys === true)
3184
	{
3185
		$message_parts = explode("\n", $message);
3186
		for ($i = 0, $n = count($message_parts); $i < $n; $i += 2)
3187
			parsesmileys($message_parts[$i]);
3188
3189
		$message = implode('', $message_parts);
3190
	}
3191
3192
	// No smileys, just get rid of the markers.
3193
	else
3194
		$message = strtr($message, array("\n" => ''));
3195
3196
	if ($message !== '' && $message[0] === ' ')
3197
		$message = '&nbsp;' . substr($message, 1);
3198
3199
	// Cleanup whitespace.
3200
	$message = strtr($message, array('  ' => ' &nbsp;', "\r" => '', "\n" => '<br>', '<br> ' => '<br>&nbsp;', '&#13;' => "\n"));
3201
3202
	// Allow mods access to what parse_bbc created
3203
	call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
3204
3205
	// Cache the output if it took some time...
3206
	if (isset($cache_key, $cache_t) && microtime(true) - $cache_t > 0.05)
3207
		cache_put_data($cache_key, $message, 240);
3208
3209
	// If this was a force parse revert if needed.
3210
	if (!empty($parse_tags))
3211
	{
3212
		$alltags_regex = empty($real_alltags_regex) ? '' : $real_alltags_regex;
3213
		unset($real_alltags_regex);
3214
	}
3215
	elseif (!empty($bbc_codes))
3216
		$bbc_lang_locales[$txt['lang_locale']] = $bbc_codes;
3217
3218
	return $message;
3219
}
3220
3221
/**
3222
 * Parse smileys in the passed message.
3223
 *
3224
 * The smiley parsing function which makes pretty faces appear :).
3225
 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
3226
 * These are specifically not parsed in code tags [url=mailto:[email protected]]
3227
 * Caches the smileys from the database or array in memory.
3228
 * Doesn't return anything, but rather modifies message directly.
3229
 *
3230
 * @param string &$message The message to parse smileys in
3231
 */
3232
function parsesmileys(&$message)
3233
{
3234
	global $modSettings, $txt, $user_info, $context, $smcFunc;
3235
	static $smileyPregSearch = null, $smileyPregReplacements = array();
3236
3237
	// No smiley set at all?!
3238
	if ($user_info['smiley_set'] == 'none' || trim($message) == '')
3239
		return;
3240
3241
	// Maybe a mod wants to implement an alternative method (e.g. emojis instead of images)
3242
	call_integration_hook('integrate_smileys', array(&$smileyPregSearch, &$smileyPregReplacements));
3243
3244
	// If smileyPregSearch hasn't been set, do it now.
3245
	if (empty($smileyPregSearch))
3246
	{
3247
		// Cache for longer when customized smiley codes aren't enabled
3248
		$cache_time = empty($modSettings['smiley_enable']) ? 7200 : 480;
3249
3250
		// Load the smileys in reverse order by length so they don't get parsed incorrectly.
3251
		if (($temp = cache_get_data('parsing_smileys_' . $user_info['smiley_set'], $cache_time)) == null)
3252
		{
3253
			$result = $smcFunc['db_query']('', '
3254
				SELECT s.code, f.filename, s.description
3255
				FROM {db_prefix}smileys AS s
3256
					JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
3257
				WHERE f.smiley_set = {string:smiley_set}' . (empty($modSettings['smiley_enable']) ? '
3258
					AND s.code IN ({array_string:default_codes})' : '') . '
3259
				ORDER BY LENGTH(s.code) DESC',
3260
				array(
3261
					'default_codes' => array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'),
3262
					'smiley_set' => $user_info['smiley_set'],
3263
				)
3264
			);
3265
			$smileysfrom = array();
3266
			$smileysto = array();
3267
			$smileysdescs = array();
3268
			while ($row = $smcFunc['db_fetch_assoc']($result))
3269
			{
3270
				$smileysfrom[] = $row['code'];
3271
				$smileysto[] = $smcFunc['htmlspecialchars']($row['filename']);
3272
				$smileysdescs[] = !empty($txt['icon_' . strtolower($row['description'])]) ? $txt['icon_' . strtolower($row['description'])] : $row['description'];
3273
			}
3274
			$smcFunc['db_free_result']($result);
3275
3276
			cache_put_data('parsing_smileys_' . $user_info['smiley_set'], array($smileysfrom, $smileysto, $smileysdescs), $cache_time);
3277
		}
3278
		else
3279
			list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
3280
3281
		// The non-breaking-space is a complex thing...
3282
		$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
3283
3284
		// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:[email protected]] doesn't parse the :D smiley)
3285
		$smileyPregReplacements = array();
3286
		$searchParts = array();
3287
		$smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
3288
3289
		for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
3290
		{
3291
			$specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES);
3292
			$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">';
3293
3294
			$smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
3295
3296
			$searchParts[] = $smileysfrom[$i];
3297
			if ($smileysfrom[$i] != $specialChars)
3298
			{
3299
				$smileyPregReplacements[$specialChars] = $smileyCode;
3300
				$searchParts[] = $specialChars;
3301
3302
				// Some 2.0 hex htmlchars are in there as 3 digits; allow for finding leading 0 or not
3303
				$specialChars2 = preg_replace('/&#(\d{2});/', '&#0$1;', $specialChars);
3304
				if ($specialChars2 != $specialChars)
3305
				{
3306
					$smileyPregReplacements[$specialChars2] = $smileyCode;
3307
					$searchParts[] = $specialChars2;
3308
				}
3309
			}
3310
		}
3311
3312
		$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

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

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

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

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

5573
			$fetch_data->get_url_data($url, /** @scrutinizer ignore-type */ $post_data);
Loading history...
5574
5575
			// no errors and a 200 result, then we have a good dataset, well we at least have data. ;)
5576
			if ($fetch_data->result('code') == 200 && !$fetch_data->result('error'))
5577
				$data = $fetch_data->result('body');
5578
			else
5579
				return false;
5580
		}
5581
5582
		// Neither fsockopen nor curl are available. Well, phooey.
5583
		else
5584
			return false;
5585
	}
5586
	else
5587
	{
5588
		// Umm, this shouldn't happen?
5589
		trigger_error('fetch_web_data(): Bad URL', E_USER_NOTICE);
5590
		$data = false;
5591
	}
5592
5593
	return $data;
5594
}
5595
5596
/**
5597
 * Attempts to determine the MIME type of some data or a file.
5598
 *
5599
 * @param string $data The data to check, or the path or URL of a file to check.
5600
 * @param string $is_path If true, $data is a path or URL to a file.
5601
 * @return string|bool A MIME type, or false if we cannot determine it.
5602
 */
5603
function get_mime_type($data, $is_path = false)
5604
{
5605
	global $cachedir;
5606
5607
	$finfo_loaded = extension_loaded('fileinfo');
5608
	$exif_loaded = extension_loaded('exif') && function_exists('image_type_to_mime_type');
5609
5610
	// Oh well. We tried.
5611
	if (!$finfo_loaded && !$exif_loaded)
5612
		return false;
5613
5614
	// Start with the 'empty' MIME type.
5615
	$mime_type = 'application/x-empty';
5616
5617
	if ($finfo_loaded)
5618
	{
5619
		// Just some nice, simple data to analyze.
5620
		if (empty($is_path))
5621
			$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
5622
5623
		// A file, or maybe a URL?
5624
		else
5625
		{
5626
			// Local file.
5627
			if (file_exists($data))
5628
				$mime_type = mime_content_type($data);
5629
5630
			// URL.
5631
			elseif ($data = fetch_web_data($data))
5632
				$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
5633
		}
5634
	}
5635
	// Workaround using Exif requires a local file.
5636
	else
5637
	{
5638
		// If $data is a URL to fetch, do so.
5639
		if (!empty($is_path) && !file_exists($data) && url_exists($data))
5640
		{
5641
			$data = fetch_web_data($data);
5642
			$is_path = false;
5643
		}
5644
5645
		// If we don't have a local file, create one and use it.
5646
		if (empty($is_path))
5647
		{
5648
			$temp_file = tempnam($cachedir, md5($data));
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type false; however, parameter $str of md5() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

5654
		$imagetype = @exif_imagetype(/** @scrutinizer ignore-type */ $data);
Loading history...
5655
5656
		if (isset($temp_file))
5657
			unlink($temp_file);
5658
5659
		// Unfortunately, this workaround only works for image files.
5660
		if ($imagetype !== false)
5661
			$mime_type = image_type_to_mime_type($imagetype);
5662
	}
5663
5664
	return $mime_type;
5665
}
5666
5667
/**
5668
 * Checks whether a file or data has the expected MIME type.
5669
 *
5670
 * @param string $data The data to check, or the path or URL of a file to check.
5671
 * @param string $type_pattern A regex pattern to match the acceptable MIME types.
5672
 * @param string $is_path If true, $data is a path or URL to a file.
5673
 * @return int 1 if the detected MIME type matches the pattern, 0 if it doesn't, or 2 if we can't check.
5674
 */
5675
function check_mime_type($data, $type_pattern, $is_path = false)
5676
{
5677
	// Get the MIME type.
5678
	$mime_type = get_mime_type($data, $is_path);
0 ignored issues
show
Bug introduced by
It seems like $is_path can also be of type false; however, parameter $is_path of get_mime_type() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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

5805
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5806
5807
	// remove left to right / right to left overrides
5808
	if ($num === 0x202D || $num === 0x202E)
5809
		return '';
5810
5811
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
5812
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
5813
		return '&#' . $num . ';';
5814
5815
	if (empty($context['utf8']))
5816
	{
5817
		// no control characters
5818
		if ($num < 0x20)
5819
			return '';
5820
		// text is text
5821
		elseif ($num < 0x80)
5822
			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

5822
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5823
		// all others get html-ised
5824
		else
5825
			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

5825
			return '&#' . /** @scrutinizer ignore-type */ $matches[2] . ';';
Loading history...
5826
	}
5827
	else
5828
	{
5829
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
5830
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
5831
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
5832
			return '';
5833
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
5834
		elseif ($num < 0x80)
5835
			return chr($num);
5836
		// <0x800 (2048)
5837
		elseif ($num < 0x800)
5838
			return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5839
		// < 0x10000 (65536)
5840
		elseif ($num < 0x10000)
5841
			return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5842
		// <= 0x10FFFF (1114111)
5843
		else
5844
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5845
	}
5846
}
5847
5848
/**
5849
 * Converts html entities to utf8 equivalents
5850
 *
5851
 * Callback function for preg_replace_callback
5852
 * Uses capture group 1 in the supplied array
5853
 * Does basic checks to keep characters inside a viewable range.
5854
 *
5855
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
5856
 * @return string The fixed string
5857
 */
5858
function fixchar__callback($matches)
5859
{
5860
	if (!isset($matches[1]))
5861
		return '';
5862
5863
	$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

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

5871
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5872
	// <0x800 (2048)
5873
	elseif ($num < 0x800)
5874
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5875
	// < 0x10000 (65536)
5876
	elseif ($num < 0x10000)
5877
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5878
	// <= 0x10FFFF (1114111)
5879
	else
5880
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5881
}
5882
5883
/**
5884
 * Strips out invalid html entities, replaces others with html style &#123; codes
5885
 *
5886
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
5887
 * strpos, strlen, substr etc
5888
 *
5889
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
5890
 * @return string The fixed string
5891
 */
5892
function entity_fix__callback($matches)
5893
{
5894
	if (!isset($matches[2]))
5895
		return '';
5896
5897
	$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

5897
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5898
5899
	// we don't allow control characters, characters out of range, byte markers, etc
5900
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
5901
		return '';
5902
	else
5903
		return '&#' . $num . ';';
5904
}
5905
5906
/**
5907
 * Return a Gravatar URL based on
5908
 * - the supplied email address,
5909
 * - the global maximum rating,
5910
 * - the global default fallback,
5911
 * - maximum sizes as set in the admin panel.
5912
 *
5913
 * It is SSL aware, and caches most of the parameters.
5914
 *
5915
 * @param string $email_address The user's email address
5916
 * @return string The gravatar URL
5917
 */
5918
function get_gravatar_url($email_address)
5919
{
5920
	global $modSettings, $smcFunc;
5921
	static $url_params = null;
5922
5923
	if ($url_params === null)
5924
	{
5925
		$ratings = array('G', 'PG', 'R', 'X');
5926
		$defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank');
5927
		$url_params = array();
5928
		if (!empty($modSettings['gravatarMaxRating']) && in_array($modSettings['gravatarMaxRating'], $ratings))
5929
			$url_params[] = 'rating=' . $modSettings['gravatarMaxRating'];
5930
		if (!empty($modSettings['gravatarDefault']) && in_array($modSettings['gravatarDefault'], $defaults))
5931
			$url_params[] = 'default=' . $modSettings['gravatarDefault'];
5932
		if (!empty($modSettings['avatar_max_width_external']))
5933
			$size_string = (int) $modSettings['avatar_max_width_external'];
5934
		if (!empty($modSettings['avatar_max_height_external']) && !empty($size_string))
5935
			if ((int) $modSettings['avatar_max_height_external'] < $size_string)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $size_string does not seem to be defined for all execution paths leading up to this point.
Loading history...
5936
				$size_string = $modSettings['avatar_max_height_external'];
5937
5938
		if (!empty($size_string))
5939
			$url_params[] = 's=' . $size_string;
5940
	}
5941
	$http_method = !empty($modSettings['force_ssl']) ? 'https://secure' : 'http://www';
5942
5943
	return $http_method . '.gravatar.com/avatar/' . md5($smcFunc['strtolower']($email_address)) . '?' . implode('&', $url_params);
5944
}
5945
5946
/**
5947
 * Get a list of timezones.
5948
 *
5949
 * @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'.
5950
 * @return array An array of timezone info.
5951
 */
5952
function smf_list_timezones($when = 'now')
5953
{
5954
	global $modSettings, $tztxt, $txt, $context, $cur_profile, $sourcedir;
5955
	static $timezones_when = array();
5956
5957
	require_once($sourcedir . '/Subs-Timezones.php');
5958
5959
	// Parseable datetime string?
5960
	if (is_int($timestamp = strtotime($when)))
0 ignored issues
show
introduced by
The condition is_int($timestamp = strtotime($when)) is always true.
Loading history...
5961
		$when = $timestamp;
5962
5963
	// A Unix timestamp?
5964
	elseif (is_numeric($when))
5965
		$when = intval($when);
5966
5967
	// Invalid value? Just get current Unix timestamp.
5968
	else
5969
		$when = time();
5970
5971
	// No point doing this over if we already did it once
5972
	if (isset($timezones_when[$when]))
5973
		return $timezones_when[$when];
5974
5975
	// We'll need these too
5976
	$date_when = date_create('@' . $when);
5977
	$later = strtotime('@' . $when . ' + 1 year');
5978
5979
	// Load up any custom time zone descriptions we might have
5980
	loadLanguage('Timezones');
5981
5982
	$tzid_metazones = get_tzid_metazones();
5983
5984
	// Should we put time zones from certain countries at the top of the list?
5985
	$priority_countries = !empty($modSettings['timezone_priority_countries']) ? explode(',', $modSettings['timezone_priority_countries']) : array();
5986
5987
	$priority_tzids = array();
5988
	foreach ($priority_countries as $country)
5989
	{
5990
		$country_tzids = get_sorted_tzids_for_country($country);
5991
5992
		if (!empty($country_tzids))
5993
			$priority_tzids = array_merge($priority_tzids, $country_tzids);
5994
	}
5995
5996
	// Antarctic research stations should be listed last, unless you're running a penguin forum
5997
	$low_priority_tzids = !in_array('AQ', $priority_countries) ? timezone_identifiers_list(DateTimeZone::ANTARCTICA) : array();
5998
5999
	$normal_priority_tzids = array_diff(array_unique(array_merge(array_keys($tzid_metazones), timezone_identifiers_list())), $priority_tzids, $low_priority_tzids);
0 ignored issues
show
Bug introduced by
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

5999
	$normal_priority_tzids = array_diff(array_unique(array_merge(array_keys($tzid_metazones), timezone_identifiers_list())), $priority_tzids, /** @scrutinizer ignore-type */ $low_priority_tzids);
Loading history...
Bug introduced by
It seems like timezone_identifiers_list() can also be of type false; however, parameter $array2 of array_merge() does only seem to accept array|null, 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

5999
	$normal_priority_tzids = array_diff(array_unique(array_merge(array_keys($tzid_metazones), /** @scrutinizer ignore-type */ timezone_identifiers_list())), $priority_tzids, $low_priority_tzids);
Loading history...
6000
6001
	// Process the preferred timezones first, then the normal ones, then the low priority ones.
6002
	$tzids = array_merge($priority_tzids, array('UTC'), $normal_priority_tzids, $low_priority_tzids);
0 ignored issues
show
Bug introduced by
It seems like $low_priority_tzids can also be of type false; however, parameter $_ of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

6002
	$tzids = array_merge($priority_tzids, array('UTC'), $normal_priority_tzids, /** @scrutinizer ignore-type */ $low_priority_tzids);
Loading history...
6003
6004
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
6005
	$dst_types = array();
6006
	$labels = array();
6007
	$offsets = array();
6008
	foreach ($tzids as $tzid)
6009
	{
6010
		// We don't want UTC right now
6011
		if ($tzid == 'UTC')
6012
			continue;
6013
6014
		$tz = timezone_open($tzid);
6015
6016
		// First, get the set of transition rules for this tzid
6017
		$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

6017
		$tzinfo = timezone_transitions_get(/** @scrutinizer ignore-type */ $tz, $when, $later);
Loading history...
6018
6019
		// Use the entire set of transition rules as the array *key* so we can avoid duplicates
6020
		$tzkey = serialize($tzinfo);
6021
6022
		// ...But make sure to include all explicitly defined meta-zones.
6023
		if (isset($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid]))
6024
			$tzkey = serialize(array_merge($tzinfo, array('metazone' => $tzid_metazones[$tzid])));
0 ignored issues
show
Bug introduced by
It seems like $tzinfo can also be of type false; however, parameter $array1 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

6024
			$tzkey = serialize(array_merge(/** @scrutinizer ignore-type */ $tzinfo, array('metazone' => $tzid_metazones[$tzid])));
Loading history...
6025
6026
		// Don't overwrite our preferred tzids
6027
		if (empty($zones[$tzkey]['tzid']))
6028
		{
6029
			$zones[$tzkey]['tzid'] = $tzid;
6030
			$zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
0 ignored issues
show
Bug introduced by
It seems like $tzinfo 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

6030
			$zones[$tzkey]['dst_type'] = count(/** @scrutinizer ignore-type */ $tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
Loading history...
6031
6032
			foreach ($tzinfo as $transition) {
6033
				$zones[$tzkey]['abbrs'][] = $transition['abbr'];
6034
			}
6035
6036
			if (isset($tzid_metazones[$tzid]))
6037
				$zones[$tzkey]['metazone'] = $tzid_metazones[$tzid];
6038
			else
6039
			{
6040
				$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

6040
				$tzgeo = timezone_location_get(/** @scrutinizer ignore-type */ $tz);
Loading history...
6041
				$country_tzids = get_sorted_tzids_for_country($tzgeo['country_code']);
6042
6043
				if (count($country_tzids) === 1)
6044
					$zones[$tzkey]['metazone'] = $txt['iso3166'][$tzgeo['country_code']];
6045
			}
6046
		}
6047
6048
		// A time zone from a prioritized country?
6049
		if (in_array($tzid, $priority_tzids))
6050
			$priority_zones[$tzkey] = true;
6051
6052
		// Keep track of the location and offset for this tzid
6053
		if (!empty($txt[$tzid]))
6054
			$zones[$tzkey]['locations'][] = $txt[$tzid];
6055
		else
6056
		{
6057
			$tzid_parts = explode('/', $tzid);
6058
			$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
6059
		}
6060
		$offsets[$tzkey] = $tzinfo[0]['offset'];
6061
6062
		// Figure out the "metazone" info for the label
6063
		if (empty($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid]))
6064
		{
6065
			$zones[$tzkey]['metazone'] = $tzid_metazones[$tzid];
6066
			$zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
6067
		}
6068
		$dst_types[$tzkey] = count($tzinfo) > 1 ? 'c' : ($tzinfo[0]['isdst'] ? 't' : 'f');
6069
		$labels[$tzkey] = !empty($zones[$tzkey]['metazone']) && !empty($tztxt[$zones[$tzkey]['metazone']]) ? $tztxt[$zones[$tzkey]['metazone']] : '';
6070
6071
		// Remember this for later
6072
		if (isset($cur_profile['timezone']) && $cur_profile['timezone'] == $tzid)
6073
			$member_tzkey = $tzkey;
6074
		if (isset($context['event']['tz']) && $context['event']['tz'] == $tzid)
6075
			$event_tzkey = $tzkey;
6076
	}
6077
6078
	// Sort by offset, then label, then DST type.
6079
	array_multisort($offsets, SORT_ASC, SORT_NUMERIC, $labels, SORT_ASC, $dst_types, SORT_ASC, $zones);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $zones seems to be defined by a foreach iteration on line 6008. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
6080
6081
	// Build the final array of formatted values
6082
	$priority_timezones = array();
6083
	$timezones = array();
6084
	foreach ($zones as $tzkey => $tzvalue)
6085
	{
6086
		date_timezone_set($date_when, timezone_open($tzvalue['tzid']));
0 ignored issues
show
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

6086
		date_timezone_set($date_when, /** @scrutinizer ignore-type */ timezone_open($tzvalue['tzid']));
Loading history...
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

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

6631
		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

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

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

7365
				$value = substr($value, 0, /** @scrutinizer ignore-type */ $param_max - strlen($key) - 5);
Loading history...
7366
		}
7367
7368
		return $array;
7369
	}
7370
}
7371
7372
/**
7373
 * array_length Recursive
7374
 * @param array $array
7375
 * @param int $deep How many levels should the function
7376
 * @return int
7377
 */
7378
function array_length($array, $deep = 3)
7379
{
7380
	// Work with arrays
7381
	$array = (array) $array;
7382
	$length = 0;
7383
7384
	$deep_count = $deep - 1;
7385
7386
	foreach ($array as $value)
7387
	{
7388
		// Recursive?
7389
		if (is_array($value))
7390
		{
7391
			// No can't do
7392
			if ($deep_count <= 0)
7393
				continue;
7394
7395
			$length += array_length($value, $deep_count);
7396
		}
7397
		else
7398
			$length += strlen($value);
7399
	}
7400
7401
	return $length;
7402
}
7403
7404
/**
7405
 * Compares existance request variables against an array.
7406
 *
7407
 * The input array is associative, where keys denote accepted values
7408
 * in a request variable denoted by `$req_val`. Values can be:
7409
 *
7410
 * - another associative array where at least one key must be found
7411
 *   in the request and their values are accepted request values.
7412
 * - A scalar value, in which case no furthur checks are done.
7413
 *
7414
 * @param array $array
7415
 * @param string $req_var request variable
7416
 *
7417
 * @return bool whether any of the criteria was satisfied
7418
 */
7419
function is_filtered_request(array $array, $req_var)
7420
{
7421
	$matched = false;
7422
	if (isset($_REQUEST[$req_var], $array[$_REQUEST[$req_var]]))
7423
	{
7424
		if (is_array($array[$_REQUEST[$req_var]]))
7425
		{
7426
			foreach ($array[$_REQUEST[$req_var]] as $subtype => $subnames)
7427
				$matched |= isset($_REQUEST[$subtype]) && in_array($_REQUEST[$subtype], $subnames);
7428
		}
7429
		else
7430
			$matched = true;
7431
	}
7432
7433
	return (bool) $matched;
7434
}
7435
7436
/**
7437
 * Clean up the XML to make sure it doesn't contain invalid characters.
7438
 *
7439
 * See https://www.w3.org/TR/xml/#charsets
7440
 *
7441
 * @param string $string The string to clean
7442
 * @return string The cleaned string
7443
 */
7444
function cleanXml($string)
7445
{
7446
	global $context;
7447
7448
	$illegal_chars = array(
7449
		// Remove all ASCII control characters except \t, \n, and \r.
7450
		"\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08",
7451
		"\x0B", "\x0C", "\x0E", "\x0F", "\x10", "\x11", "\x12", "\x13", "\x14",
7452
		"\x15", "\x16", "\x17", "\x18", "\x19", "\x1A", "\x1B", "\x1C", "\x1D",
7453
		"\x1E", "\x1F",
7454
		// Remove \xFFFE and \xFFFF
7455
		"\xEF\xBF\xBE", "\xEF\xBF\xBF",
7456
	);
7457
7458
	$string = str_replace($illegal_chars, '', $string);
7459
7460
	// The Unicode surrogate pair code points should never be present in our
7461
	// strings to begin with, but if any snuck in, they need to be removed.
7462
	if (!empty($context['utf8']) && strpos($string, "\xED") !== false)
7463
		$string = preg_replace('/\xED[\xA0-\xBF][\x80-\xBF]/', '', $string);
7464
7465
	return $string;
7466
}
7467
7468
/**
7469
 * Escapes (replaces) characters in strings to make them safe for use in javascript
7470
 *
7471
 * @param string $string The string to escape
7472
 * @return string The escaped string
7473
 */
7474
function JavaScriptEscape($string)
7475
{
7476
	global $scripturl;
7477
7478
	return '\'' . strtr($string, array(
7479
		"\r" => '',
7480
		"\n" => '\\n',
7481
		"\t" => '\\t',
7482
		'\\' => '\\\\',
7483
		'\'' => '\\\'',
7484
		'</' => '<\' + \'/',
7485
		'<script' => '<scri\'+\'pt',
7486
		'<body>' => '<bo\'+\'dy>',
7487
		'<a href' => '<a hr\'+\'ef',
7488
		$scripturl => '\' + smf_scripturl + \'',
7489
	)) . '\'';
7490
}
7491
7492
?>