fixchar__callback()   C
last analyzed

Complexity

Conditions 12

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 13
nop 1
dl 0
loc 23
rs 6.9666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

1545
						for ($php_i = 0, $php_n = count(/** @scrutinizer ignore-type */ $php_parts); $php_i < $php_n; $php_i++)
Loading history...
1546
						{
1547
							// Do PHP code coloring?
1548
							if ($php_parts[$php_i] != '&lt;?php')
1549
								continue;
1550
1551
							$php_string = '';
1552
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1553
							{
1554
								$php_string .= $php_parts[$php_i];
1555
								$php_parts[$php_i++] = '';
1556
							}
1557
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1558
						}
1559
1560
						// Fix the PHP code stuff...
1561
						$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

1561
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', /** @scrutinizer ignore-type */ $php_parts));
Loading history...
1562
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1563
1564
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1565
						if (!empty($context['browser']['is_opera']))
1566
							$data .= '&nbsp;';
1567
					}
1568
				},
1569
				'block_level' => true,
1570
			),
1571
			array(
1572
				'tag' => 'code',
1573
				'type' => 'unparsed_equals_content',
1574
				'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>',
1575
				// @todo Maybe this can be simplified?
1576
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1577
				{
1578
					if (!isset($disabled['code']))
1579
					{
1580
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1581
1582
						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

1582
						for ($php_i = 0, $php_n = count(/** @scrutinizer ignore-type */ $php_parts); $php_i < $php_n; $php_i++)
Loading history...
1583
						{
1584
							// Do PHP code coloring?
1585
							if ($php_parts[$php_i] != '&lt;?php')
1586
								continue;
1587
1588
							$php_string = '';
1589
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1590
							{
1591
								$php_string .= $php_parts[$php_i];
1592
								$php_parts[$php_i++] = '';
1593
							}
1594
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1595
						}
1596
1597
						// Fix the PHP code stuff...
1598
						$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

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

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

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

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

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

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

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

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

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

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

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

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

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

5702
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5703
5704
	// remove left to right / right to left overrides
5705
	if ($num === 0x202D || $num === 0x202E)
5706
		return '';
5707
5708
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
5709
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
5710
		return '&#' . $num . ';';
5711
5712
	if (empty($context['utf8']))
5713
	{
5714
		// no control characters
5715
		if ($num < 0x20)
5716
			return '';
5717
		// text is text
5718
		elseif ($num < 0x80)
5719
			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

5719
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5720
		// all others get html-ised
5721
		else
5722
			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

5722
			return '&#' . /** @scrutinizer ignore-type */ $matches[2] . ';';
Loading history...
5723
	}
5724
	else
5725
	{
5726
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
5727
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
5728
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
5729
			return '';
5730
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
5731
		elseif ($num < 0x80)
5732
			return chr($num);
5733
		// <0x800 (2048)
5734
		elseif ($num < 0x800)
5735
			return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5736
		// < 0x10000 (65536)
5737
		elseif ($num < 0x10000)
5738
			return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5739
		// <= 0x10FFFF (1114111)
5740
		else
5741
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5742
	}
5743
}
5744
5745
/**
5746
 * Converts html entities to utf8 equivalents
5747
 *
5748
 * Callback function for preg_replace_callback
5749
 * Uses capture group 1 in the supplied array
5750
 * Does basic checks to keep characters inside a viewable range.
5751
 *
5752
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
5753
 * @return string The fixed string
5754
 */
5755
function fixchar__callback($matches)
5756
{
5757
	if (!isset($matches[1]))
5758
		return '';
5759
5760
	$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

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

5768
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5769
	// <0x800 (2048)
5770
	elseif ($num < 0x800)
5771
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5772
	// < 0x10000 (65536)
5773
	elseif ($num < 0x10000)
5774
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5775
	// <= 0x10FFFF (1114111)
5776
	else
5777
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5778
}
5779
5780
/**
5781
 * Strips out invalid html entities, replaces others with html style &#123; codes
5782
 *
5783
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
5784
 * strpos, strlen, substr etc
5785
 *
5786
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
5787
 * @return string The fixed string
5788
 */
5789
function entity_fix__callback($matches)
5790
{
5791
	if (!isset($matches[2]))
5792
		return '';
5793
5794
	$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

5794
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5795
5796
	// we don't allow control characters, characters out of range, byte markers, etc
5797
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
5798
		return '';
5799
	else
5800
		return '&#' . $num . ';';
5801
}
5802
5803
/**
5804
 * Return a Gravatar URL based on
5805
 * - the supplied email address,
5806
 * - the global maximum rating,
5807
 * - the global default fallback,
5808
 * - maximum sizes as set in the admin panel.
5809
 *
5810
 * It is SSL aware, and caches most of the parameters.
5811
 *
5812
 * @param string $email_address The user's email address
5813
 * @return string The gravatar URL
5814
 */
5815
function get_gravatar_url($email_address)
5816
{
5817
	global $modSettings, $smcFunc;
5818
	static $url_params = null;
5819
5820
	if ($url_params === null)
5821
	{
5822
		$ratings = array('G', 'PG', 'R', 'X');
5823
		$defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank');
5824
		$url_params = array();
5825
		if (!empty($modSettings['gravatarMaxRating']) && in_array($modSettings['gravatarMaxRating'], $ratings))
5826
			$url_params[] = 'rating=' . $modSettings['gravatarMaxRating'];
5827
		if (!empty($modSettings['gravatarDefault']) && in_array($modSettings['gravatarDefault'], $defaults))
5828
			$url_params[] = 'default=' . $modSettings['gravatarDefault'];
5829
		if (!empty($modSettings['avatar_max_width_external']))
5830
			$size_string = (int) $modSettings['avatar_max_width_external'];
5831
		if (!empty($modSettings['avatar_max_height_external']) && !empty($size_string))
5832
			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...
5833
				$size_string = $modSettings['avatar_max_height_external'];
5834
5835
		if (!empty($size_string))
5836
			$url_params[] = 's=' . $size_string;
5837
	}
5838
	$http_method = !empty($modSettings['force_ssl']) ? 'https://secure' : 'http://www';
5839
5840
	return $http_method . '.gravatar.com/avatar/' . md5($smcFunc['strtolower']($email_address)) . '?' . implode('&', $url_params);
5841
}
5842
5843
/**
5844
 * Get a list of timezones.
5845
 *
5846
 * @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'.
5847
 * @return array An array of timezone info.
5848
 */
5849
function smf_list_timezones($when = 'now')
5850
{
5851
	global $smcFunc, $modSettings, $tztxt, $txt;
5852
	static $timezones = null, $lastwhen = null;
5853
5854
	// No point doing this over if we already did it once
5855
	if (!empty($timezones) && $when == $lastwhen)
5856
		return $timezones;
5857
	else
5858
		$lastwhen = $when;
5859
5860
	// Parseable datetime string?
5861
	if (is_int($timestamp = strtotime($when)))
0 ignored issues
show
introduced by
The condition is_int($timestamp = strtotime($when)) is always true.
Loading history...
5862
		$when = $timestamp;
5863
5864
	// A Unix timestamp?
5865
	elseif (is_numeric($when))
5866
		$when = intval($when);
5867
5868
	// Invalid value? Just get current Unix timestamp.
5869
	else
5870
		$when = time();
5871
5872
	// We'll need these too
5873
	$date_when = date_create('@' . $when);
5874
	$later = (int) date_format(date_add($date_when, date_interval_create_from_date_string('1 year')), 'U');
0 ignored issues
show
Bug introduced by
It seems like $date_when can also be of type false; however, parameter $object of date_add() 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

5874
	$later = (int) date_format(date_add(/** @scrutinizer ignore-type */ $date_when, date_interval_create_from_date_string('1 year')), 'U');
Loading history...
5875
5876
	// Load up any custom time zone descriptions we might have
5877
	loadLanguage('Timezones');
5878
5879
	// Should we put time zones from certain countries at the top of the list?
5880
	$priority_countries = !empty($modSettings['timezone_priority_countries']) ? explode(',', $modSettings['timezone_priority_countries']) : array();
5881
	$priority_tzids = array();
5882
	foreach ($priority_countries as $country)
5883
	{
5884
		$country_tzids = @timezone_identifiers_list(DateTimeZone::PER_COUNTRY, strtoupper(trim($country)));
5885
		if (!empty($country_tzids))
5886
			$priority_tzids = array_merge($priority_tzids, $country_tzids);
5887
	}
5888
5889
	// Antarctic research stations should be listed last, unless you're running a penguin forum
5890
	$low_priority_tzids = !in_array('AQ', $priority_countries) ? timezone_identifiers_list(DateTimeZone::ANTARCTICA) : array();
5891
5892
	// Process the preferred timezones first, then the normal ones, then the low priority ones.
5893
	$tzids = array_merge(array_keys($tztxt), array_diff(timezone_identifiers_list(), array_keys($tztxt), $low_priority_tzids), $low_priority_tzids);
0 ignored issues
show
Bug introduced by
It seems like timezone_identifiers_list() can also be of type false; however, parameter $array1 of array_diff() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

5893
	$tzids = array_merge(array_keys($tztxt), array_diff(/** @scrutinizer ignore-type */ timezone_identifiers_list(), array_keys($tztxt), $low_priority_tzids), $low_priority_tzids);
Loading history...
Bug introduced by
It seems like $low_priority_tzids can also be of type false; however, parameter $_ of array_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

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

5893
	$tzids = array_merge(array_keys($tztxt), array_diff(timezone_identifiers_list(), array_keys($tztxt), /** @scrutinizer ignore-type */ $low_priority_tzids), $low_priority_tzids);
Loading history...
5894
5895
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
5896
	foreach ($tzids as $tzid)
5897
	{
5898
		// We don't want UTC right now
5899
		if ($tzid == 'UTC')
5900
			continue;
5901
5902
		$tz = timezone_open($tzid);
5903
5904
		// First, get the set of transition rules for this tzid
5905
		$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

5905
		$tzinfo = timezone_transitions_get(/** @scrutinizer ignore-type */ $tz, $when, $later);
Loading history...
5906
5907
		// Use the entire set of transition rules as the array *key* so we can avoid duplicates
5908
		$tzkey = serialize($tzinfo);
5909
5910
		// Next, get the geographic info for this tzid
5911
		$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

5911
		$tzgeo = timezone_location_get(/** @scrutinizer ignore-type */ $tz);
Loading history...
5912
5913
		// Don't overwrite our preferred tzids
5914
		if (empty($zones[$tzkey]['tzid']))
5915
		{
5916
			$zones[$tzkey]['tzid'] = $tzid;
5917
			$zones[$tzkey]['abbr'] = $tzinfo[0]['abbr'];
5918
		}
5919
5920
		// A time zone from a prioritized country?
5921
		if (in_array($tzid, $priority_tzids))
5922
			$priority_zones[$tzkey] = true;
5923
5924
		// Keep track of the location and offset for this tzid
5925
		if (!empty($txt[$tzid]))
5926
			$zones[$tzkey]['locations'][] = $txt[$tzid];
5927
		else
5928
		{
5929
			$tzid_parts = explode('/', $tzid);
5930
			$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
5931
		}
5932
		$offsets[$tzkey] = $tzinfo[0]['offset'];
5933
		$longitudes[$tzkey] = empty($longitudes[$tzkey]) ? $tzgeo['longitude'] : $longitudes[$tzkey];
5934
	}
5935
5936
	// Sort by offset then longitude
5937
	array_multisort($offsets, SORT_ASC, SORT_NUMERIC, $longitudes, SORT_ASC, SORT_NUMERIC, $zones);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $zones does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $longitudes does not seem to be defined for all execution paths leading up to this point.
Loading history...
5938
5939
	// Build the final array of formatted values
5940
	$priority_timezones = array();
5941
	$timezones = array();
5942
	foreach ($zones as $tzkey => $tzvalue)
5943
	{
5944
		date_timezone_set($date_when, timezone_open($tzvalue['tzid']));
0 ignored issues
show
Bug introduced by
It seems like $date_when can also be of type false; however, parameter $object of date_timezone_set() does only seem to accept DateTime, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

6451
		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

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

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

7185
                $value = substr($value, 0, /** @scrutinizer ignore-type */ $param_max - strlen($key) - 5);
Loading history...
7186
        }
7187
7188
        return $array;
7189
    }
7190
}
7191
7192
/**
7193
 * array_length Recursive
7194
 * @param $array
7195
 * @param int $deep How many levels should the function
7196
 * @return int
7197
 */
7198
function array_length($array, $deep = 3)
7199
{
7200
    // Work with arrays
7201
    $array = (array) $array;
7202
    $length = 0;
7203
7204
    $deep_count = $deep - 1;
7205
7206
    foreach ($array as $value)
7207
    {
7208
        // Recursive?
7209
        if (is_array($value))
7210
        {
7211
            // No can't do
7212
            if ($deep_count <= 0)
7213
                continue;
7214
7215
            $length += array_length($value, $deep_count);
7216
        }
7217
7218
        else
7219
            $length += strlen($value);
7220
    }
7221
7222
    return $length;
7223
}
7224
7225
?>