Completed
Push — release-2.1 ( 473bd5...c00aa2 )
by Mert
11s
created

Subs-Editor.php ➔ theme_postbox()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file contains those functions specific to the editing box and is
5
 * generally used for WYSIWYG type functionality.
6
 *
7
 * Simple Machines Forum (SMF)
8
 *
9
 * @package SMF
10
 * @author Simple Machines http://www.simplemachines.org
11
 * @copyright 2018 Simple Machines and individual contributors
12
 * @license http://www.simplemachines.org/about/smf/license.php BSD
13
 *
14
 * @version 2.1 Beta 4
15
 */
16
17
if (!defined('SMF'))
18
	die('No direct access...');
19
20
/**
21
 * As of SMF 2.1, this is unused. But it is available if any mod wants to use it.
22
 * Convert only the BBC that can be edited in HTML mode for the (old) editor.
23
 *
24
 * @deprecated since version 2.1
25
 * @param string $text The text with bbcode in it
26
 * @param boolean $compat_mode Whether to actually convert the text
27
 * @return string The text
28
 */
29
function bbc_to_html($text, $compat_mode = false)
30
{
31
	global $modSettings;
32
33
	if (!$compat_mode)
34
		return $text;
35
36
	// Turn line breaks back into br's.
37
	$text = strtr($text, array("\r" => '', "\n" => '<br>'));
38
39
	// Prevent conversion of all bbcode inside these bbcodes.
40
	// @todo Tie in with bbc permissions ?
41
	foreach (array('code', 'php', 'nobbc') as $code)
42
	{
43
		if (strpos($text, '[' . $code) !== false)
44
		{
45
			$parts = preg_split('~(\[/' . $code . '\]|\[' . $code . '(?:=[^\]]+)?\])~i', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
46
47
			// Only mess with stuff inside tags.
48
			for ($i = 0, $n = count($parts); $i < $n; $i++)
49
			{
50
				// Value of 2 means we're inside the tag.
51
				if ($i % 4 == 2)
52
					$parts[$i] = strtr($parts[$i], array('[' => '&#91;', ']' => '&#93;', "'" => "'"));
53
			}
54
			// Put our humpty dumpty message back together again.
55
			$text = implode('', $parts);
56
		}
57
	}
58
59
	// What tags do we allow?
60
	$allowed_tags = array('b', 'u', 'i', 's', 'hr', 'list', 'li', 'font', 'size', 'color', 'img', 'left', 'center', 'right', 'url', 'email', 'ftp', 'sub', 'sup');
61
62
	$text = parse_bbc($text, true, '', $allowed_tags);
63
64
	// Fix for having a line break then a thingy.
65
	$text = strtr($text, array('<br><div' => '<div', "\n" => '', "\r" => ''));
66
67
	// Note that IE doesn't understand spans really - make them something "legacy"
68
	$working_html = array(
69
		'~<del>(.+?)</del>~i' => '<strike>$1</strike>',
70
		'~<span\sclass="bbc_u">(.+?)</span>~i' => '<u>$1</u>',
71
		'~<span\sstyle="color:\s*([#\d\w]+);" class="bbc_color">(.+?)</span>~i' => '<font color="$1">$2</font>',
72
		'~<span\sstyle="font-family:\s*([#\d\w\s]+);" class="bbc_font">(.+?)</span>~i' => '<font face="$1">$2</font>',
73
		'~<div\sstyle="text-align:\s*(left|right);">(.+?)</div>~i' => '<p align="$1">$2</p>',
74
	);
75
	$text = preg_replace(array_keys($working_html), array_values($working_html), $text);
76
77
	// Parse unique ID's and disable javascript into the smileys - using the double space.
78
	$i = 1;
79
	$text = preg_replace_callback('~(?:\s|&nbsp;)?<(img\ssrc="' . preg_quote($modSettings['smileys_url'], '~') . '/[^<>]+?/([^<>]+?)"\s*)[^<>]*?class="smiley">~',
80
		function($m) use (&$i)
81
		{
82
			return '<' . stripslashes($m[1]) . 'alt="" title="" onresizestart="return false;" id="smiley_' . $i++ . '_' . $m[2] . '" style="padding: 0 3px 0 3px;">';
83
		}, $text);
84
85
	return $text;
86
}
87
88
/**
89
 * Converts HTML to BBC
90
 * As of SMF 2.1, only used by ManageBoards.php (and possibly mods)
91
 *
92
 * @param string $text Text containing HTML
93
 * @return string The text with html converted to bbc
94
 */
95
function html_to_bbc($text)
96
{
97
	global $modSettings, $smcFunc, $scripturl, $context;
98
99
	// Replace newlines with spaces, as that's how browsers usually interpret them.
100
	$text = preg_replace("~\s*[\r\n]+\s*~", ' ', $text);
101
102
	// Though some of us love paragraphs, the parser will do better with breaks.
103
	$text = preg_replace('~</p>\s*?<p~i', '</p><br><p', $text);
104
	$text = preg_replace('~</p>\s*(?!<)~i', '</p><br>', $text);
105
106
	// Safari/webkit wraps lines in Wysiwyg in <div>'s.
107
	if (isBrowser('webkit'))
108
		$text = preg_replace(array('~<div(?:\s(?:[^<>]*?))?' . '>~i', '</div>'), array('<br>', ''), $text);
109
110
	// If there's a trailing break get rid of it - Firefox tends to add one.
111
	$text = preg_replace('~<br\s?/?' . '>$~i', '', $text);
112
113
	// Remove any formatting within code tags.
114
	if (strpos($text, '[code') !== false)
115
	{
116
		$text = preg_replace('~<br\s?/?' . '>~i', '#smf_br_spec_grudge_cool!#', $text);
117
		$parts = preg_split('~(\[/code\]|\[code(?:=[^\]]+)?\])~i', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
118
119
		// Only mess with stuff outside [code] tags.
120
		for ($i = 0, $n = count($parts); $i < $n; $i++)
121
		{
122
			// Value of 2 means we're inside the tag.
123
			if ($i % 4 == 2)
124
				$parts[$i] = strip_tags($parts[$i]);
125
		}
126
127
		$text = strtr(implode('', $parts), array('#smf_br_spec_grudge_cool!#' => '<br>'));
128
	}
129
130
	// Remove scripts, style and comment blocks.
131
	$text = preg_replace('~<script[^>]*[^/]?' . '>.*?</script>~i', '', $text);
132
	$text = preg_replace('~<style[^>]*[^/]?' . '>.*?</style>~i', '', $text);
133
	$text = preg_replace('~\\<\\!--.*?-->~i', '', $text);
134
	$text = preg_replace('~\\<\\!\\[CDATA\\[.*?\\]\\]\\>~i', '', $text);
135
136
	// Do the smileys ultra first!
137
	preg_match_all('~<img\s+[^<>]*?id="*smiley_\d+_([^<>]+?)[\s"/>]\s*[^<>]*?/*>(?:\s)?~i', $text, $matches);
138
	if (!empty($matches[0]))
139
	{
140
		// Easy if it's not custom.
141
		if (empty($modSettings['smiley_enable']))
142
		{
143
			$smileysfrom = array('>:D', ':D', '::)', '>:(', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', '0:)');
144
			$smileysto = array('evil.png', 'cheesy.png', 'rolleyes.png', 'angry.png', 'smiley.png', 'wink.png', 'grin.png', 'sad.png', 'shocked.png', 'cool.png', 'tongue.png', 'huh.png', 'embarrassed.png', 'lipsrsealed.png', 'kiss.png', 'cry.png', 'undecided.png', 'azn.png', 'afro.png', 'police.png', 'angel.png');
145
146
			foreach ($matches[1] as $k => $file)
147
			{
148
				$found = array_search($file, $smileysto);
149
				// Note the weirdness here is to stop double spaces between smileys.
150
				if ($found)
0 ignored issues
show
Bug Best Practice introduced by
The expression $found of type false|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
151
					$matches[1][$k] = '-[]-smf_smily_start#|#' . $smcFunc['htmlspecialchars']($smileysfrom[$found]) . '-[]-smf_smily_end#|#';
152
				else
153
					$matches[1][$k] = '';
154
			}
155
		}
156
		else
157
		{
158
			// Load all the smileys.
159
			$names = array();
160
			foreach ($matches[1] as $file)
161
				$names[] = $file;
162
			$names = array_unique($names);
163
164
			if (!empty($names))
165
			{
166
				$request = $smcFunc['db_query']('', '
167
					SELECT code, filename
168
					FROM {db_prefix}smileys
169
					WHERE filename IN ({array_string:smiley_filenames})',
170
					array(
171
						'smiley_filenames' => $names,
172
					)
173
				);
174
				$mappings = array();
175
				while ($row = $smcFunc['db_fetch_assoc']($request))
176
					$mappings[$row['filename']] = $smcFunc['htmlspecialchars']($row['code']);
177
				$smcFunc['db_free_result']($request);
178
179
				foreach ($matches[1] as $k => $file)
180
					if (isset($mappings[$file]))
181
						$matches[1][$k] = '-[]-smf_smily_start#|#' . $mappings[$file] . '-[]-smf_smily_end#|#';
182
			}
183
		}
184
185
		// Replace the tags!
186
		$text = str_replace($matches[0], $matches[1], $text);
187
188
		// Now sort out spaces
189
		$text = str_replace(array('-[]-smf_smily_end#|#-[]-smf_smily_start#|#', '-[]-smf_smily_end#|#', '-[]-smf_smily_start#|#'), ' ', $text);
190
	}
191
192
	// Only try to buy more time if the client didn't quit.
193
	if (connection_aborted() && $context['server']['is_apache'])
194
		@apache_reset_timeout();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
195
196
	$parts = preg_split('~(<[A-Za-z]+\s*[^<>]*?style="?[^<>"]+"?[^<>]*?(?:/?)>|</[A-Za-z]+>)~', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
197
	$replacement = '';
198
	$stack = array();
199
200
	foreach ($parts as $part)
201
	{
202
		if (preg_match('~(<([A-Za-z]+)\s*[^<>]*?)style="?([^<>"]+)"?([^<>]*?(/?)>)~', $part, $matches) === 1)
203
		{
204
			// If it's being closed instantly, we can't deal with it...yet.
205
			if ($matches[5] === '/')
206
				continue;
207
			else
208
			{
209
				// Get an array of styles that apply to this element. (The strtr is there to combat HTML generated by Word.)
210
				$styles = explode(';', strtr($matches[3], array('&quot;' => '')));
211
				$curElement = $matches[2];
212
				$precedingStyle = $matches[1];
213
				$afterStyle = $matches[4];
214
				$curCloseTags = '';
215
				$extra_attr = '';
216
217
				foreach ($styles as $type_value_pair)
218
				{
219
					// Remove spaces and convert uppercase letters.
220
					$clean_type_value_pair = strtolower(strtr(trim($type_value_pair), '=', ':'));
221
222
					// Something like 'font-weight: bold' is expected here.
223
					if (strpos($clean_type_value_pair, ':') === false)
224
						continue;
225
226
					// Capture the elements of a single style item (e.g. 'font-weight' and 'bold').
227
					list ($style_type, $style_value) = explode(':', $type_value_pair);
228
229
					$style_value = trim($style_value);
230
231
					switch (trim($style_type))
232
					{
233
						case 'font-weight':
234
							if ($style_value === 'bold')
235
							{
236
								$curCloseTags .= '[/b]';
237
								$replacement .= '[b]';
238
							}
239
						break;
240
241
						case 'text-decoration':
242
							if ($style_value == 'underline')
243
							{
244
								$curCloseTags .= '[/u]';
245
								$replacement .= '[u]';
246
							}
247
							elseif ($style_value == 'line-through')
248
							{
249
								$curCloseTags .= '[/s]';
250
								$replacement .= '[s]';
251
							}
252
						break;
253
254
						case 'text-align':
255
							if ($style_value == 'left')
256
							{
257
								$curCloseTags .= '[/left]';
258
								$replacement .= '[left]';
259
							}
260
							elseif ($style_value == 'center')
261
							{
262
								$curCloseTags .= '[/center]';
263
								$replacement .= '[center]';
264
							}
265
							elseif ($style_value == 'right')
266
							{
267
								$curCloseTags .= '[/right]';
268
								$replacement .= '[right]';
269
							}
270
						break;
271
272
						case 'font-style':
273
							if ($style_value == 'italic')
274
							{
275
								$curCloseTags .= '[/i]';
276
								$replacement .= '[i]';
277
							}
278
						break;
279
280
						case 'color':
281
							$curCloseTags .= '[/color]';
282
							$replacement .= '[color=' . $style_value . ']';
283
						break;
284
285
						case 'font-size':
286
							// Sometimes people put decimals where decimals should not be.
287
							if (preg_match('~(\d)+\.\d+(p[xt])~i', $style_value, $dec_matches) === 1)
288
								$style_value = $dec_matches[1] . $dec_matches[2];
289
290
							$curCloseTags .= '[/size]';
291
							$replacement .= '[size=' . $style_value . ']';
292
						break;
293
294
						case 'font-family':
295
							// Only get the first freaking font if there's a list!
296
							if (strpos($style_value, ',') !== false)
297
								$style_value = substr($style_value, 0, strpos($style_value, ','));
298
299
							$curCloseTags .= '[/font]';
300
							$replacement .= '[font=' . strtr($style_value, array("'" => '')) . ']';
301
						break;
302
303
						// This is a hack for images with dimensions embedded.
304
						case 'width':
305
						case 'height':
306
							if (preg_match('~[1-9]\d*~i', $style_value, $dimension) === 1)
307
								$extra_attr .= ' ' . $style_type . '="' . $dimension[0] . '"';
308
						break;
309
310
						case 'list-style-type':
311
							if (preg_match('~none|disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-alpha|upper-alpha|lower-greek|lower-latin|upper-latin|hebrew|armenian|georgian|cjk-ideographic|hiragana|katakana|hiragana-iroha|katakana-iroha~i', $style_value, $listType) === 1)
312
								$extra_attr .= ' listtype="' . $listType[0] . '"';
313
						break;
314
					}
315
				}
316
317
				// Preserve some tags stripping the styling.
318
				if (in_array($matches[2], array('a', 'font', 'td')))
319
				{
320
					$replacement .= $precedingStyle . $afterStyle;
321
					$curCloseTags = '</' . $matches[2] . '>' . $curCloseTags;
322
				}
323
324
				// If there's something that still needs closing, push it to the stack.
325
				if (!empty($curCloseTags))
326
					array_push($stack, array(
327
							'element' => strtolower($curElement),
328
							'closeTags' => $curCloseTags
329
						)
330
					);
331
				elseif (!empty($extra_attr))
332
					$replacement .= $precedingStyle . $extra_attr . $afterStyle;
333
			}
334
		}
335
336
		elseif (preg_match('~</([A-Za-z]+)>~', $part, $matches) === 1)
337
		{
338
			// Is this the element that we've been waiting for to be closed?
339
			if (!empty($stack) && strtolower($matches[1]) === $stack[count($stack) - 1]['element'])
340
			{
341
				$byebyeTag = array_pop($stack);
342
				$replacement .= $byebyeTag['closeTags'];
343
			}
344
345
			// Must've been something else.
346
			else
347
				$replacement .= $part;
348
		}
349
		// In all other cases, just add the part to the replacement.
350
		else
351
			$replacement .= $part;
352
	}
353
354
	// Now put back the replacement in the text.
355
	$text = $replacement;
356
357
	// We are not finished yet, request more time.
358
	if (connection_aborted() && $context['server']['is_apache'])
359
		@apache_reset_timeout();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
360
361
	// Let's pull out any legacy alignments.
362
	while (preg_match('~<([A-Za-z]+)\s+[^<>]*?(align="*(left|center|right)"*)[^<>]*?(/?)>~i', $text, $matches) === 1)
363
	{
364
		// Find the position in the text of this tag over again.
365
		$start_pos = strpos($text, $matches[0]);
366
		if ($start_pos === false)
367
			break;
368
369
		// End tag?
370
		if ($matches[4] != '/' && strpos($text, '</' . $matches[1] . '>', $start_pos) !== false)
371
		{
372
			$end_pos = strpos($text, '</' . $matches[1] . '>', $start_pos);
373
374
			// Remove the align from that tag so it's never checked again.
375
			$tag = substr($text, $start_pos, strlen($matches[0]));
376
			$content = substr($text, $start_pos + strlen($matches[0]), $end_pos - $start_pos - strlen($matches[0]));
377
			$tag = str_replace($matches[2], '', $tag);
378
379
			// Put the tags back into the body.
380
			$text = substr($text, 0, $start_pos) . $tag . '[' . $matches[3] . ']' . $content . '[/' . $matches[3] . ']' . substr($text, $end_pos);
381
		}
382
		else
383
		{
384
			// Just get rid of this evil tag.
385
			$text = substr($text, 0, $start_pos) . substr($text, $start_pos + strlen($matches[0]));
386
		}
387
	}
388
389
	// Let's do some special stuff for fonts - cause we all love fonts.
390
	while (preg_match('~<font\s+([^<>]*)>~i', $text, $matches) === 1)
391
	{
392
		// Find the position of this again.
393
		$start_pos = strpos($text, $matches[0]);
394
		$end_pos = false;
395
		if ($start_pos === false)
396
			break;
397
398
		// This must have an end tag - and we must find the right one.
399
		$lower_text = strtolower($text);
400
401
		$start_pos_test = $start_pos + 4;
402
		// How many starting tags must we find closing ones for first?
403
		$start_font_tag_stack = 0;
404
		while ($start_pos_test < strlen($text))
405
		{
406
			// Where is the next starting font?
407
			$next_start_pos = strpos($lower_text, '<font', $start_pos_test);
408
			$next_end_pos = strpos($lower_text, '</font>', $start_pos_test);
409
410
			// Did we past another starting tag before an end one?
411
			if ($next_start_pos !== false && $next_start_pos < $next_end_pos)
412
			{
413
				$start_font_tag_stack++;
414
				$start_pos_test = $next_start_pos + 4;
415
			}
416
			// Otherwise we have an end tag but not the right one?
417
			elseif ($start_font_tag_stack)
418
			{
419
				$start_font_tag_stack--;
420
				$start_pos_test = $next_end_pos + 4;
421
			}
422
			// Otherwise we're there!
423
			else
424
			{
425
				$end_pos = $next_end_pos;
426
				break;
427
			}
428
		}
429
		if ($end_pos === false)
430
			break;
431
432
		// Now work out what the attributes are.
433
		$attribs = fetchTagAttributes($matches[1]);
434
		$tags = array();
435
		$sizes_equivalence = array(1 => '8pt', '10pt', '12pt', '14pt', '18pt', '24pt', '36pt');
436
		foreach ($attribs as $s => $v)
437
		{
438
			if ($s == 'size')
439
			{
440
				// Cast before empty chech because casting a string results in a 0 and we don't have zeros in the array! ;)
441
				$v = (int) trim($v);
442
				$v = empty($v) ? 1 : $v;
443
				$tags[] = array('[size=' . $sizes_equivalence[$v] . ']', '[/size]');
444
			}
445 View Code Duplication
			elseif ($s == 'face')
446
				$tags[] = array('[font=' . trim(strtolower($v)) . ']', '[/font]');
447 View Code Duplication
			elseif ($s == 'color')
448
				$tags[] = array('[color=' . trim(strtolower($v)) . ']', '[/color]');
449
		}
450
451
		// As before add in our tags.
452
		$before = $after = '';
453
		foreach ($tags as $tag)
454
		{
455
			$before .= $tag[0];
456
			if (isset($tag[1]))
457
				$after = $tag[1] . $after;
458
		}
459
460
		// Remove the tag so it's never checked again.
461
		$content = substr($text, $start_pos + strlen($matches[0]), $end_pos - $start_pos - strlen($matches[0]));
462
463
		// Put the tags back into the body.
464
		$text = substr($text, 0, $start_pos) . $before . $content . $after . substr($text, $end_pos + 7);
465
	}
466
467
	// Almost there, just a little more time.
468
	if (connection_aborted() && $context['server']['is_apache'])
469
		@apache_reset_timeout();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
470
471
	if (count($parts = preg_split('~<(/?)(li|ol|ul)([^>]*)>~i', $text, null, PREG_SPLIT_DELIM_CAPTURE)) > 1)
472
	{
473
		// A toggle that dermines whether we're directly under a <ol> or <ul>.
474
		$inList = false;
475
476
		// Keep track of the number of nested list levels.
477
		$listDepth = 0;
478
479
		// Map what we can expect from the HTML to what is supported by SMF.
480
		$listTypeMapping = array(
481
			'1' => 'decimal',
482
			'A' => 'upper-alpha',
483
			'a' => 'lower-alpha',
484
			'I' => 'upper-roman',
485
			'i' => 'lower-roman',
486
			'disc' => 'disc',
487
			'square' => 'square',
488
			'circle' => 'circle',
489
		);
490
491
		// $i: text, $i + 1: '/', $i + 2: tag, $i + 3: tail.
492
		for ($i = 0, $numParts = count($parts) - 1; $i < $numParts; $i += 4)
493
		{
494
			$tag = strtolower($parts[$i + 2]);
495
			$isOpeningTag = $parts[$i + 1] === '';
496
497
			if ($isOpeningTag)
498
			{
499
				switch ($tag)
500
				{
501
					case 'ol':
502
					case 'ul':
503
504
						// We have a problem, we're already in a list.
505
						if ($inList)
506
						{
507
							// Inject a list opener, we'll deal with the ol/ul next loop.
508
							array_splice($parts, $i, 0, array(
509
								'',
510
								'',
511
								str_repeat("\t", $listDepth) . '[li]',
512
								'',
513
							));
514
							$numParts = count($parts) - 1;
515
516
							// The inlist status changes a bit.
517
							$inList = false;
518
						}
519
520
						// Just starting a new list.
521
						else
522
						{
523
							$inList = true;
524
525
							if ($tag === 'ol')
526
								$listType = 'decimal';
527
							elseif (preg_match('~type="?(' . implode('|', array_keys($listTypeMapping)) . ')"?~', $parts[$i + 3], $match) === 1)
528
								$listType = $listTypeMapping[$match[1]];
529
							else
530
								$listType = null;
531
532
							$listDepth++;
533
534
							$parts[$i + 2] = '[list' . ($listType === null ? '' : ' type=' . $listType) . ']' . "\n";
535
							$parts[$i + 3] = '';
536
						}
537
					break;
538
539
					case 'li':
540
541
						// This is how it should be: a list item inside the list.
542
						if ($inList)
543
						{
544
							$parts[$i + 2] = str_repeat("\t", $listDepth) . '[li]';
545
							$parts[$i + 3] = '';
546
547
							// Within a list item, it's almost as if you're outside.
548
							$inList = false;
549
						}
550
551
						// The li is no direct child of a list.
552
						else
553
						{
554
							// We are apparently in a list item.
555
							if ($listDepth > 0)
556
							{
557
								$parts[$i + 2] = '[/li]' . "\n" . str_repeat("\t", $listDepth) . '[li]';
558
								$parts[$i + 3] = '';
559
							}
560
561
							// We're not even near a list.
562
							else
563
							{
564
								// Quickly create a list with an item.
565
								$listDepth++;
566
567
								$parts[$i + 2] = '[list]' . "\n\t" . '[li]';
568
								$parts[$i + 3] = '';
569
							}
570
						}
571
572
					break;
573
				}
574
			}
575
576
			// Handle all the closing tags.
577
			else
578
			{
579
				switch ($tag)
580
				{
581
					case 'ol':
582
					case 'ul':
583
584
						// As we expected it, closing the list while we're in it.
585
						if ($inList)
586
						{
587
							$inList = false;
588
589
							$listDepth--;
590
591
							$parts[$i + 1] = '';
592
							$parts[$i + 2] = str_repeat("\t", $listDepth) . '[/list]';
593
							$parts[$i + 3] = '';
594
						}
595
596
						else
597
						{
598
							// We're in a list item.
599
							if ($listDepth > 0)
600
							{
601
								// Inject closure for this list item first.
602
								// The content of $parts[$i] is left as is!
603
								array_splice($parts, $i + 1, 0, array(
604
									'', // $i + 1
605
									'[/li]' . "\n", // $i + 2
606
									'', // $i + 3
607
									'', // $i + 4
608
								));
609
								$numParts = count($parts) - 1;
610
611
								// Now that we've closed the li, we're in list space.
612
								$inList = true;
613
							}
614
615
							// We're not even in a list, ignore
616 View Code Duplication
							else
617
							{
618
								$parts[$i + 1] = '';
619
								$parts[$i + 2] = '';
620
								$parts[$i + 3] = '';
621
							}
622
						}
623
					break;
624
625
					case 'li':
626
627
						if ($inList)
628
						{
629
							// There's no use for a </li> after <ol> or <ul>, ignore.
630
							$parts[$i + 1] = '';
631
							$parts[$i + 2] = '';
632
							$parts[$i + 3] = '';
633
						}
634
635
						else
636
						{
637
							// Remove the trailing breaks from the list item.
638
							$parts[$i] = preg_replace('~\s*<br\s*' . '/?' . '>\s*$~', '', $parts[$i]);
639
							$parts[$i + 1] = '';
640
							$parts[$i + 2] = '[/li]' . "\n";
641
							$parts[$i + 3] = '';
642
643
							// And we're back in the [list] space.
644
							$inList = true;
645
						}
646
647
					break;
648
				}
649
			}
650
651
			// If we're in the [list] space, no content is allowed.
652
			if ($inList && trim(preg_replace('~\s*<br\s*' . '/?' . '>\s*~', '', $parts[$i + 4])) !== '')
653
			{
654
				// Fix it by injecting an extra list item.
655
				array_splice($parts, $i + 4, 0, array(
656
					'', // No content.
657
					'', // Opening tag.
658
					'li', // It's a <li>.
659
					'', // No tail.
660
				));
661
				$numParts = count($parts) - 1;
662
			}
663
		}
664
665
		$text = implode('', $parts);
666
667
		if ($inList)
668
		{
669
			$listDepth--;
670
			$text .= str_repeat("\t", $listDepth) . '[/list]';
671
		}
672
673
		for ($i = $listDepth; $i > 0; $i--)
674
			$text .= '[/li]' . "\n" . str_repeat("\t", $i - 1) . '[/list]';
675
	}
676
677
	// I love my own image...
678
	while (preg_match('~<img\s+([^<>]*)/*>~i', $text, $matches) === 1)
679
	{
680
		// Find the position of the image.
681
		$start_pos = strpos($text, $matches[0]);
682
		if ($start_pos === false)
683
			break;
684
		$end_pos = $start_pos + strlen($matches[0]);
685
686
		$params = '';
687
		$src = '';
688
689
		$attrs = fetchTagAttributes($matches[1]);
690
		foreach ($attrs as $attrib => $value)
691
		{
692
			if (in_array($attrib, array('width', 'height')))
693
				$params .= ' ' . $attrib . '=' . (int) $value;
694
			elseif ($attrib == 'alt' && trim($value) != '')
695
				$params .= ' alt=' . trim($value);
696
			elseif ($attrib == 'src')
697
				$src = trim($value);
698
		}
699
700
		$tag = '';
701
		if (!empty($src))
702
		{
703
			// Attempt to fix the path in case it's not present.
704 View Code Duplication
			if (preg_match('~^https?://~i', $src) === 0 && is_array($parsedURL = parse_url($scripturl)) && isset($parsedURL['host']))
705
			{
706
				$baseURL = (isset($parsedURL['scheme']) ? $parsedURL['scheme'] : 'http') . '://' . $parsedURL['host'] . (empty($parsedURL['port']) ? '' : ':' . $parsedURL['port']);
707
708
				if (substr($src, 0, 1) === '/')
709
					$src = $baseURL . $src;
710
				else
711
					$src = $baseURL . (empty($parsedURL['path']) ? '/' : preg_replace('~/(?:index\\.php)?$~', '', $parsedURL['path'])) . '/' . $src;
712
			}
713
714
			$tag = '[img' . $params . ']' . $src . '[/img]';
715
		}
716
717
		// Replace the tag
718
		$text = substr($text, 0, $start_pos) . $tag . substr($text, $end_pos);
719
	}
720
721
	// The final bits are the easy ones - tags which map to tags which map to tags - etc etc.
722
	$tags = array(
723
		'~<b(\s(.)*?)*?' . '>~i' => function()
724
		{
725
			return '[b]';
726
		},
727
		'~</b>~i' => function()
728
		{
729
			return '[/b]';
730
		},
731
		'~<i(\s(.)*?)*?' . '>~i' => function()
732
		{
733
			return '[i]';
734
		},
735
		'~</i>~i' => function()
736
		{
737
			return '[/i]';
738
		},
739
		'~<u(\s(.)*?)*?' . '>~i' => function()
740
		{
741
			return '[u]';
742
		},
743
		'~</u>~i' => function()
744
		{
745
			return '[/u]';
746
		},
747
		'~<strong(\s(.)*?)*?' . '>~i' => function()
748
		{
749
			return '[b]';
750
		},
751
		'~</strong>~i' => function()
752
		{
753
			return '[/b]';
754
		},
755
		'~<em(\s(.)*?)*?' . '>~i' => function()
756
		{
757
			return '[i]';
758
		},
759
		'~</em>~i' => function()
760
		{
761
			return '[i]';
762
		},
763
		'~<s(\s(.)*?)*?' . '>~i' => function()
764
		{
765
			return "[s]";
766
		},
767
		'~</s>~i' => function()
768
		{
769
			return "[/s]";
770
		},
771
		'~<strike(\s(.)*?)*?' . '>~i' => function()
772
		{
773
			return '[s]';
774
		},
775
		'~</strike>~i' => function()
776
		{
777
			return '[/s]';
778
		},
779
		'~<del(\s(.)*?)*?' . '>~i' => function()
780
		{
781
			return '[s]';
782
		},
783
		'~</del>~i' => function()
784
		{
785
			return '[/s]';
786
		},
787
		'~<center(\s(.)*?)*?' . '>~i' => function()
788
		{
789
			return '[center]';
790
		},
791
		'~</center>~i' => function()
792
		{
793
			return '[/center]';
794
		},
795
		'~<pre(\s(.)*?)*?' . '>~i' => function()
796
		{
797
			return '[pre]';
798
		},
799
		'~</pre>~i' => function()
800
		{
801
			return '[/pre]';
802
		},
803
		'~<sub(\s(.)*?)*?' . '>~i' => function()
804
		{
805
			return '[sub]';
806
		},
807
		'~</sub>~i' => function()
808
		{
809
			return '[/sub]';
810
		},
811
		'~<sup(\s(.)*?)*?' . '>~i' => function()
812
		{
813
			return '[sup]';
814
		},
815
		'~</sup>~i' => function()
816
		{
817
			return '[/sup]';
818
		},
819
		'~<tt(\s(.)*?)*?' . '>~i' => function()
820
		{
821
			return '[tt]';
822
		},
823
		'~</tt>~i' => function()
824
		{
825
			return '[/tt]';
826
		},
827
		'~<table(\s(.)*?)*?' . '>~i' => function()
828
		{
829
			return '[table]';
830
		},
831
		'~</table>~i' => function()
832
		{
833
			return '[/table]';
834
		},
835
		'~<tr(\s(.)*?)*?' . '>~i' => function()
836
		{
837
			return '[tr]';
838
		},
839
		'~</tr>~i' => function()
840
		{
841
			return '[/tr]';
842
		},
843
		'~<(td|th)\s[^<>]*?colspan="?(\d{1,2})"?.*?' . '>~i' => function($matches)
844
		{
845
			return str_repeat('[td][/td]', $matches[2] - 1) . '[td]';
846
		},
847
		'~<(td|th)(\s(.)*?)*?' . '>~i' => function()
848
		{
849
			return '[td]';
850
		},
851
		'~</(td|th)>~i' => function()
852
		{
853
			return '[/td]';
854
		},
855
		'~<br(?:\s[^<>]*?)?' . '>~i' => function()
856
		{
857
			return "\n";
858
		},
859
		'~<hr[^<>]*>(\n)?~i' => function($matches)
860
		{
861
			return "[hr]\n" . $matches[0];
862
		},
863
		'~(\n)?\\[hr\\]~i' => function()
864
		{
865
			return "\n[hr]";
866
		},
867
		'~^\n\\[hr\\]~i' => function()
868
		{
869
			return "[hr]";
870
		},
871
		'~<blockquote(\s(.)*?)*?' . '>~i' =>  function()
872
		{
873
			return "&lt;blockquote&gt;";
874
		},
875
		'~</blockquote>~i' => function()
876
		{
877
			return "&lt;/blockquote&gt;";
878
		},
879
		'~<ins(\s(.)*?)*?' . '>~i' => function()
880
		{
881
			return "&lt;ins&gt;";
882
		},
883
		'~</ins>~i' => function()
884
		{
885
			return "&lt;/ins&gt;";
886
		},
887
	);
888
889
	foreach ($tags as $tag => $replace)
890
		$text = preg_replace_callback($tag, $replace, $text);
891
892
	// Please give us just a little more time.
893
	if (connection_aborted() && $context['server']['is_apache'])
894
		@apache_reset_timeout();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
895
896
	// What about URL's - the pain in the ass of the tag world.
897
	while (preg_match('~<a\s+([^<>]*)>([^<>]*)</a>~i', $text, $matches) === 1)
898
	{
899
		// Find the position of the URL.
900
		$start_pos = strpos($text, $matches[0]);
901
		if ($start_pos === false)
902
			break;
903
		$end_pos = $start_pos + strlen($matches[0]);
904
905
		$tag_type = 'url';
906
		$href = '';
907
908
		$attrs = fetchTagAttributes($matches[1]);
909
		foreach ($attrs as $attrib => $value)
910
		{
911
			if ($attrib == 'href')
912
			{
913
				$href = trim($value);
914
915
				// Are we dealing with an FTP link?
916
				if (preg_match('~^ftps?://~', $href) === 1)
917
					$tag_type = 'ftp';
918
919
				// Or is this a link to an email address?
920
				elseif (substr($href, 0, 7) == 'mailto:')
921
				{
922
					$tag_type = 'email';
923
					$href = substr($href, 7);
924
				}
925
926
				// No http(s), so attempt to fix this potential relative URL.
927 View Code Duplication
				elseif (preg_match('~^https?://~i', $href) === 0 && is_array($parsedURL = parse_url($scripturl)) && isset($parsedURL['host']))
928
				{
929
					$baseURL = (isset($parsedURL['scheme']) ? $parsedURL['scheme'] : 'http') . '://' . $parsedURL['host'] . (empty($parsedURL['port']) ? '' : ':' . $parsedURL['port']);
930
931
					if (substr($href, 0, 1) === '/')
932
						$href = $baseURL . $href;
933
					else
934
						$href = $baseURL . (empty($parsedURL['path']) ? '/' : preg_replace('~/(?:index\\.php)?$~', '', $parsedURL['path'])) . '/' . $href;
935
				}
936
			}
937
938
			// External URL?
939
			if ($attrib == 'target' && $tag_type == 'url')
940
			{
941
				if (trim($value) == '_blank')
942
					$tag_type == 'iurl';
943
			}
944
		}
945
946
		$tag = '';
947
		if ($href != '')
948
		{
949
			if ($matches[2] == $href)
950
				$tag = '[' . $tag_type . ']' . $href . '[/' . $tag_type . ']';
951
			else
952
				$tag = '[' . $tag_type . '=' . $href . ']' . $matches[2] . '[/' . $tag_type . ']';
953
		}
954
955
		// Replace the tag
956
		$text = substr($text, 0, $start_pos) . $tag . substr($text, $end_pos);
957
	}
958
959
	$text = strip_tags($text);
960
961
	// Some tags often end up as just dummy tags - remove those.
962
	$text = preg_replace('~\[[bisu]\]\s*\[/[bisu]\]~', '', $text);
963
964
	// Fix up entities.
965
	$text = preg_replace('~&#38;~i', '&#38;#38;', $text);
966
967
	$text = legalise_bbc($text);
968
969
	return $text;
970
}
971
972
/**
973
 * Returns an array of attributes associated with a tag.
974
 *
975
 * @param string $text A tag
976
 * @return array An array of attributes
977
 */
978
function fetchTagAttributes($text)
979
{
980
	$attribs = array();
981
	$key = $value = '';
982
	$tag_state = 0; // 0 = key, 1 = attribute with no string, 2 = attribute with string
983
	for ($i = 0; $i < strlen($text); $i++)
984
	{
985
		// We're either moving from the key to the attribute or we're in a string and this is fine.
986
		if ($text[$i] == '=')
987
		{
988
			if ($tag_state == 0)
989
				$tag_state = 1;
990
			elseif ($tag_state == 2)
991
				$value .= '=';
992
		}
993
		// A space is either moving from an attribute back to a potential key or in a string is fine.
994
		elseif ($text[$i] == ' ')
995
		{
996
			if ($tag_state == 2)
997
				$value .= ' ';
998
			elseif ($tag_state == 1)
999
			{
1000
				$attribs[$key] = $value;
1001
				$key = $value = '';
1002
				$tag_state = 0;
1003
			}
1004
		}
1005
		// A quote?
1006
		elseif ($text[$i] == '"')
1007
		{
1008
			// Must be either going into or out of a string.
1009
			if ($tag_state == 1)
1010
				$tag_state = 2;
1011
			else
1012
				$tag_state = 1;
1013
		}
1014
		// Otherwise it's fine.
1015
		else
1016
		{
1017
			if ($tag_state == 0)
1018
				$key .= $text[$i];
1019
			else
1020
				$value .= $text[$i];
1021
		}
1022
	}
1023
1024
	// Anything left?
1025
	if ($key != '' && $value != '')
1026
		$attribs[$key] = $value;
1027
1028
	return $attribs;
1029
}
1030
1031
/**
1032
 * Attempt to clean up illegal BBC caused by browsers like Opera which don't obey the rules
1033
 *
1034
 * @param string $text Text
1035
 * @return string Cleaned up text
1036
 */
1037
function legalise_bbc($text)
1038
{
1039
	global $modSettings;
1040
1041
	// Don't care about the texts that are too short.
1042
	if (strlen($text) < 3)
1043
		return $text;
1044
1045
	// A list of tags that's disabled by the admin.
1046
	$disabled = empty($modSettings['disabledBBC']) ? array() : array_flip(explode(',', strtolower($modSettings['disabledBBC'])));
1047
1048
	// Get a list of all the tags that are not disabled.
1049
	$all_tags = parse_bbc(false);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1050
	$valid_tags = array();
1051
	$self_closing_tags = array();
1052
	foreach ($all_tags as $tag)
0 ignored issues
show
Bug introduced by
The expression $all_tags of type string is not traversable.
Loading history...
1053
	{
1054
		if (!isset($disabled[$tag['tag']]))
1055
			$valid_tags[$tag['tag']] = !empty($tag['block_level']);
1056
		if (isset($tag['type']) && $tag['type'] == 'closed')
1057
			$self_closing_tags[] = $tag['tag'];
1058
	}
1059
1060
	// Right - we're going to start by going through the whole lot to make sure we don't have align stuff crossed as this happens load and is stupid!
1061
	$align_tags = array('left', 'center', 'right', 'pre');
1062
1063
	// Remove those align tags that are not valid.
1064
	$align_tags = array_intersect($align_tags, array_keys($valid_tags));
1065
1066
	// These keep track of where we are!
1067
	if (!empty($align_tags) && count($matches = preg_split('~(\\[/?(?:' . implode('|', $align_tags) . ')\\])~', $text, -1, PREG_SPLIT_DELIM_CAPTURE)) > 1)
1068
	{
1069
		// The first one is never a tag.
1070
		$isTag = false;
1071
1072
		// By default we're not inside a tag too.
1073
		$insideTag = null;
1074
1075
		foreach ($matches as $i => $match)
1076
		{
1077
			// We're only interested in tags, not text.
1078
			if ($isTag)
1079
			{
1080
				$isClosingTag = substr($match, 1, 1) === '/';
1081
				$tagName = substr($match, $isClosingTag ? 2 : 1, -1);
1082
1083
				// We're closing the exact same tag that we opened.
1084
				if ($isClosingTag && $insideTag === $tagName)
1085
					$insideTag = null;
1086
1087
				// We're opening a tag and we're not yet inside one either
1088
				elseif (!$isClosingTag && $insideTag === null)
1089
					$insideTag = $tagName;
1090
1091
				// In all other cases, this tag must be invalid
1092
				else
1093
					unset($matches[$i]);
1094
			}
1095
1096
			// The next one is gonna be the other one.
1097
			$isTag = !$isTag;
1098
		}
1099
1100
		// We're still inside a tag and had no chance for closure?
1101
		if ($insideTag !== null)
1102
			$matches[] = '[/' . $insideTag . ']';
1103
1104
		// And a complete text string again.
1105
		$text = implode('', $matches);
1106
	}
1107
1108
	// Quickly remove any tags which are back to back.
1109
	$backToBackPattern = '~\\[(' . implode('|', array_diff(array_keys($valid_tags), array('td', 'anchor'))) . ')[^<>\\[\\]]*\\]\s*\\[/\\1\\]~';
1110
	$lastlen = 0;
1111 View Code Duplication
	while (strlen($text) !== $lastlen)
1112
		$lastlen = strlen($text = preg_replace($backToBackPattern, '', $text));
1113
1114
	// Need to sort the tags by name length.
1115
	uksort($valid_tags, function ($a, $b) {
1116
		return strlen($a) < strlen($b) ? 1 : -1;
1117
	});
1118
1119
	// These inline tags can compete with each other regarding style.
1120
	$competing_tags = array(
1121
		'color',
1122
		'size',
1123
	);
1124
1125
	// These keep track of where we are!
1126
	if (count($parts = preg_split(sprintf('~(\\[)(/?)(%1$s)((?:[\\s=][^\\]\\[]*)?\\])~', implode('|', array_keys($valid_tags))), $text, -1, PREG_SPLIT_DELIM_CAPTURE)) > 1)
1127
	{
1128
		// Start outside [nobbc] or [code] blocks.
1129
		$inCode = false;
1130
		$inNoBbc = false;
1131
1132
		// A buffer containing all opened inline elements.
1133
		$inlineElements = array();
1134
1135
		// A buffer containing all opened block elements.
1136
		$blockElements = array();
1137
1138
		// A buffer containing the opened inline elements that might compete.
1139
		$competingElements = array();
1140
1141
		// $i: text, $i + 1: '[', $i + 2: '/', $i + 3: tag, $i + 4: tag tail.
1142
		for ($i = 0, $n = count($parts) - 1; $i < $n; $i += 5)
1143
		{
1144
			$tag = $parts[$i + 3];
1145
			$isOpeningTag = $parts[$i + 2] === '';
1146
			$isClosingTag = $parts[$i + 2] === '/';
1147
			$isBlockLevelTag = isset($valid_tags[$tag]) && $valid_tags[$tag] && !in_array($tag, $self_closing_tags);
1148
			$isCompetingTag = in_array($tag, $competing_tags);
1149
1150
			// Check if this might be one of those cleaned out tags.
1151
			if ($tag === '')
1152
				continue;
1153
1154
			// Special case: inside [code] blocks any code is left untouched.
1155 View Code Duplication
			elseif ($tag === 'code')
1156
			{
1157
				// We're inside a code block and closing it.
1158
				if ($inCode && $isClosingTag)
1159
				{
1160
					$inCode = false;
1161
1162
					// Reopen tags that were closed before the code block.
1163
					if (!empty($inlineElements))
1164
						$parts[$i + 4] .= '[' . implode('][', array_keys($inlineElements)) . ']';
1165
				}
1166
1167
				// We're outside a coding and nobbc block and opening it.
1168
				elseif (!$inCode && !$inNoBbc && $isOpeningTag)
1169
				{
1170
					// If there are still inline elements left open, close them now.
1171
					if (!empty($inlineElements))
1172
					{
1173
						$parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']';
1174
						//$inlineElements = array();
1175
					}
1176
1177
					$inCode = true;
1178
				}
1179
1180
				// Nothing further to do.
1181
				continue;
1182
			}
1183
1184
			// Special case: inside [nobbc] blocks any BBC is left untouched.
1185 View Code Duplication
			elseif ($tag === 'nobbc')
1186
			{
1187
				// We're inside a nobbc block and closing it.
1188
				if ($inNoBbc && $isClosingTag)
1189
				{
1190
					$inNoBbc = false;
1191
1192
					// Some inline elements might've been closed that need reopening.
1193
					if (!empty($inlineElements))
1194
						$parts[$i + 4] .= '[' . implode('][', array_keys($inlineElements)) . ']';
1195
				}
1196
1197
				// We're outside a nobbc and coding block and opening it.
1198
				elseif (!$inNoBbc && !$inCode && $isOpeningTag)
1199
				{
1200
					// Can't have inline elements still opened.
1201
					if (!empty($inlineElements))
1202
					{
1203
						$parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']';
1204
						//$inlineElements = array();
1205
					}
1206
1207
					$inNoBbc = true;
1208
				}
1209
1210
				continue;
1211
			}
1212
1213
			// So, we're inside one of the special blocks: ignore any tag.
1214
			elseif ($inCode || $inNoBbc)
1215
				continue;
1216
1217
			// We're dealing with an opening tag.
1218
			if ($isOpeningTag)
1219
			{
1220
				// Everyting inside the square brackets of the opening tag.
1221
				$elementContent = $parts[$i + 3] . substr($parts[$i + 4], 0, -1);
1222
1223
				// A block level opening tag.
1224
				if ($isBlockLevelTag)
1225
				{
1226
					// Are there inline elements still open?
1227 View Code Duplication
					if (!empty($inlineElements))
1228
					{
1229
						// Close all the inline tags, a block tag is coming...
1230
						$parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']';
1231
1232
						// Now open them again, we're inside the block tag now.
1233
						$parts[$i + 5] = '[' . implode('][', array_keys($inlineElements)) . ']' . $parts[$i + 5];
1234
					}
1235
1236
					$blockElements[] = $tag;
1237
				}
1238
1239
				// Inline opening tag.
1240
				elseif (!in_array($tag, $self_closing_tags))
1241
				{
1242
					// Can't have two opening elements with the same contents!
1243
					if (isset($inlineElements[$elementContent]))
1244
					{
1245
						// Get rid of this tag.
1246
						$parts[$i + 1] = $parts[$i + 2] = $parts[$i + 3] = $parts[$i + 4] = '';
1247
1248
						// Now try to find the corresponding closing tag.
1249
						$curLevel = 1;
1250
						for ($j = $i + 5, $m = count($parts) - 1; $j < $m; $j += 5)
1251
						{
1252
							// Find the tags with the same tagname
1253
							if ($parts[$j + 3] === $tag)
1254
							{
1255
								// If it's an opening tag, increase the level.
1256
								if ($parts[$j + 2] === '')
1257
									$curLevel++;
1258
1259
								// A closing tag, decrease the level.
1260
								else
1261
								{
1262
									$curLevel--;
1263
1264
									// Gotcha! Clean out this closing tag gone rogue.
1265
									if ($curLevel === 0)
1266
									{
1267
										$parts[$j + 1] = $parts[$j + 2] = $parts[$j + 3] = $parts[$j + 4] = '';
1268
										break;
1269
									}
1270
								}
1271
							}
1272
						}
1273
					}
1274
1275
					// Otherwise, add this one to the list.
1276
					else
1277
					{
1278
						if ($isCompetingTag)
1279
						{
1280
							if (!isset($competingElements[$tag]))
1281
								$competingElements[$tag] = array();
1282
1283
							$competingElements[$tag][] = $parts[$i + 4];
1284
1285
							if (count($competingElements[$tag]) > 1)
1286
								$parts[$i] .= '[/' . $tag . ']';
1287
						}
1288
1289
						$inlineElements[$elementContent] = $tag;
1290
					}
1291
				}
1292
			}
1293
1294
			// Closing tag.
1295
			else
1296
			{
1297
				// Closing the block tag.
1298
				if ($isBlockLevelTag)
1299
				{
1300
					// Close the elements that should've been closed by closing this tag.
1301
					if (!empty($blockElements))
1302
					{
1303
						$addClosingTags = array();
1304
						while ($element = array_pop($blockElements))
1305
						{
1306
							if ($element === $tag)
1307
								break;
1308
1309
							// Still a block tag was open not equal to this tag.
1310
							$addClosingTags[] = $element['type'];
1311
						}
1312
1313
						if (!empty($addClosingTags))
1314
							$parts[$i + 1] = '[/' . implode('][/', array_reverse($addClosingTags)) . ']' . $parts[$i + 1];
1315
1316
						// Apparently the closing tag was not found on the stack.
1317
						if (!is_string($element) || $element !== $tag)
1318
						{
1319
							// Get rid of this particular closing tag, it was never opened.
1320
							$parts[$i + 1] = substr($parts[$i + 1], 0, -1);
1321
							$parts[$i + 2] = $parts[$i + 3] = $parts[$i + 4] = '';
1322
							continue;
1323
						}
1324
					}
1325 View Code Duplication
					else
1326
					{
1327
						// Get rid of this closing tag!
1328
						$parts[$i + 1] = $parts[$i + 2] = $parts[$i + 3] = $parts[$i + 4] = '';
1329
						continue;
1330
					}
1331
1332
					// Inline elements are still left opened?
1333 View Code Duplication
					if (!empty($inlineElements))
1334
					{
1335
						// Close them first..
1336
						$parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']';
1337
1338
						// Then reopen them.
1339
						$parts[$i + 5] = '[' . implode('][', array_keys($inlineElements)) . ']' . $parts[$i + 5];
1340
					}
1341
				}
1342
				// Inline tag.
1343
				else
1344
				{
1345
					// Are we expecting this tag to end?
1346
					if (in_array($tag, $inlineElements))
1347
					{
1348
						foreach (array_reverse($inlineElements, true) as $tagContentToBeClosed => $tagToBeClosed)
1349
						{
1350
							// Closing it one way or the other.
1351
							unset($inlineElements[$tagContentToBeClosed]);
1352
1353
							// Was this the tag we were looking for?
1354
							if ($tagToBeClosed === $tag)
1355
								break;
1356
1357
							// Nope, close it and look further!
1358
							else
1359
								$parts[$i] .= '[/' . $tagToBeClosed . ']';
1360
						}
1361
1362
						if ($isCompetingTag && !empty($competingElements[$tag]))
1363
						{
1364
							array_pop($competingElements[$tag]);
1365
1366
							if (count($competingElements[$tag]) > 0)
1367
								$parts[$i + 5] = '[' . $tag . $competingElements[$tag][count($competingElements[$tag]) - 1] . $parts[$i + 5];
1368
						}
1369
					}
1370
1371
					// Unexpected closing tag, ex-ter-mi-nate.
1372 View Code Duplication
					else
1373
						$parts[$i + 1] = $parts[$i + 2] = $parts[$i + 3] = $parts[$i + 4] = '';
1374
				}
1375
			}
1376
		}
1377
1378
		// Close the code tags.
1379
		if ($inCode)
1380
			$parts[$i] .= '[/code]';
1381
1382
		// The same for nobbc tags.
1383
		elseif ($inNoBbc)
1384
			$parts[$i] .= '[/nobbc]';
1385
1386
		// Still inline tags left unclosed? Close them now, better late than never.
1387
		elseif (!empty($inlineElements))
1388
			$parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']';
1389
1390
		// Now close the block elements.
1391
		if (!empty($blockElements))
1392
			$parts[$i] .= '[/' . implode('][/', array_reverse($blockElements)) . ']';
1393
1394
		$text = implode('', $parts);
1395
	}
1396
1397
	// Final clean up of back to back tags.
1398
	$lastlen = 0;
1399 View Code Duplication
	while (strlen($text) !== $lastlen)
1400
		$lastlen = strlen($text = preg_replace($backToBackPattern, '', $text));
1401
1402
	return $text;
1403
}
1404
1405
/**
1406
 * Creates the javascript code for localization of the editor (SCEditor)
1407
 */
1408
function loadLocale()
1409
{
1410
	global $context, $txt, $editortxt, $modSettings;
1411
1412
	loadLanguage('Editor');
1413
1414
	$context['template_layers'] = array();
1415
	// Lets make sure we aren't going to output anything nasty.
1416
	@ob_end_clean();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1417
	if (!empty($modSettings['enableCompressedOutput']))
1418
		@ob_start('ob_gzhandler');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1419
	else
1420
		@ob_start();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1421
1422
	// If we don't have any locale better avoid broken js
1423
	if (empty($txt['lang_locale']))
1424
		die();
1425
1426
	$file_data = '(function ($) {
1427
	\'use strict\';
1428
1429
	$.sceditor.locale[' . JavaScriptEscape($txt['lang_locale']) . '] = {';
1430
	foreach ($editortxt as $key => $val)
1431
		$file_data .= '
1432
		' . JavaScriptEscape($key) . ': ' . JavaScriptEscape($val) . ',';
1433
1434
	$file_data .= '
1435
		dateFormat: "day.month.year"
1436
	}
1437
})(jQuery);';
1438
1439
	// Make sure they know what type of file we are.
1440
	header('content-type: text/javascript');
1441
	echo $file_data;
1442
	obExit(false);
1443
}
1444
1445
/**
1446
 * Retrieves a list of message icons.
1447
 * - Based on the settings, the array will either contain a list of default
1448
 *   message icons or a list of custom message icons retrieved from the database.
1449
 * - The board_id is needed for the custom message icons (which can be set for
1450
 *   each board individually).
1451
 *
1452
 * @param int $board_id The ID of the board
1453
 * @return array An array of info about available icons
1454
 */
1455
function getMessageIcons($board_id)
1456
{
1457
	global $modSettings, $txt, $settings, $smcFunc;
1458
1459
	if (empty($modSettings['messageIcons_enable']))
1460
	{
1461
		loadLanguage('Post');
1462
1463
		$icons = array(
1464
			array('value' => 'xx', 'name' => $txt['standard']),
1465
			array('value' => 'thumbup', 'name' => $txt['thumbs_up']),
1466
			array('value' => 'thumbdown', 'name' => $txt['thumbs_down']),
1467
			array('value' => 'exclamation', 'name' => $txt['exclamation_point']),
1468
			array('value' => 'question', 'name' => $txt['question_mark']),
1469
			array('value' => 'lamp', 'name' => $txt['lamp']),
1470
			array('value' => 'smiley', 'name' => $txt['icon_smiley']),
1471
			array('value' => 'angry', 'name' => $txt['icon_angry']),
1472
			array('value' => 'cheesy', 'name' => $txt['icon_cheesy']),
1473
			array('value' => 'grin', 'name' => $txt['icon_grin']),
1474
			array('value' => 'sad', 'name' => $txt['icon_sad']),
1475
			array('value' => 'wink', 'name' => $txt['icon_wink']),
1476
			array('value' => 'poll', 'name' => $txt['icon_poll']),
1477
		);
1478
1479
		foreach ($icons as $k => $dummy)
1480
		{
1481
			$icons[$k]['url'] = $settings['images_url'] . '/post/' . $dummy['value'] . '.png';
1482
			$icons[$k]['is_last'] = false;
1483
		}
1484
	}
1485
	// Otherwise load the icons, and check we give the right image too...
1486
	else
1487
	{
1488
		if (($temp = cache_get_data('posting_icons-' . $board_id, 480)) == null)
1489
		{
1490
			$request = $smcFunc['db_query']('', '
1491
				SELECT title, filename
1492
				FROM {db_prefix}message_icons
1493
				WHERE id_board IN (0, {int:board_id})
1494
				ORDER BY icon_order',
1495
				array(
1496
					'board_id' => $board_id,
1497
				)
1498
			);
1499
			$icon_data = array();
1500
			while ($row = $smcFunc['db_fetch_assoc']($request))
1501
				$icon_data[] = $row;
1502
			$smcFunc['db_free_result']($request);
1503
1504
			$icons = array();
1505
			foreach ($icon_data as $icon)
1506
			{
1507
				$icons[$icon['filename']] = array(
1508
					'value' => $icon['filename'],
1509
					'name' => $icon['title'],
1510
					'url' => $settings[file_exists($settings['theme_dir'] . '/images/post/' . $icon['filename'] . '.png') ? 'images_url' : 'default_images_url'] . '/post/' . $icon['filename'] . '.png',
1511
					'is_last' => false,
1512
				);
1513
			}
1514
1515
			cache_put_data('posting_icons-' . $board_id, $icons, 480);
1516
		}
1517
		else
1518
			$icons = $temp;
1519
	}
1520
	call_integration_hook('integrate_load_message_icons', array(&$icons));
1521
1522
	return array_values($icons);
1523
}
1524
1525
/**
1526
 * Creates a box that can be used for richedit stuff like BBC, Smileys etc.
1527
 * @param array $editorOptions Various options for the editor
1528
 */
1529
function create_control_richedit($editorOptions)
1530
{
1531
	global $txt, $modSettings, $options, $smcFunc, $editortxt;
1532
	global $context, $settings, $user_info, $scripturl;
1533
1534
	// Load the Post language file... for the moment at least.
1535
	loadLanguage('Post');
1536
	loadLanguage('Editor');
1537
1538
	// Every control must have a ID!
1539
	assert(isset($editorOptions['id']));
1540
	assert(isset($editorOptions['value']));
1541
1542
	// Is this the first richedit - if so we need to ensure some template stuff is initialised.
1543
	if (empty($context['controls']['richedit']))
1544
	{
1545
		// Some general stuff.
1546
		$settings['smileys_url'] = $modSettings['smileys_url'] . '/' . $user_info['smiley_set'];
1547
		if (!empty($context['drafts_autosave']))
1548
			$context['drafts_autosave_frequency'] = empty($modSettings['drafts_autosave_frequency']) ? 60000 : $modSettings['drafts_autosave_frequency'] * 1000;
1549
1550
		// This really has some WYSIWYG stuff.
1551
		loadCSSFile('jquery.sceditor.css', array('force_current' => false, 'validate' => true), 'smf_jquery_sceditor');
1552
		loadTemplate('GenericControls');
1553
1554
		// JS makes the editor go round
1555
		loadJavaScriptFile('editor.js', array('minimize' => true), 'smf_editor');
1556
		loadJavaScriptFile('jquery.sceditor.bbcode.min.js', array(), 'smf_sceditor_bbcode');
1557
		loadJavaScriptFile('jquery.sceditor.smf.js', array('minimize' => true), 'smf_sceditor_smf');
1558
		loadJavaScriptFile('sceditor.undo.js', array('minimize' => true), 'smf_sceditor_undo');
1559
		addInlineJavaScript('
1560
		var smf_smileys_url = \'' . $settings['smileys_url'] . '\';
1561
		var bbc_quote_from = \'' . addcslashes($txt['quote_from'], "'") . '\';
1562
		var bbc_quote = \'' . addcslashes($txt['quote'], "'") . '\';
1563
		var bbc_search_on = \'' . addcslashes($txt['search_on'], "'") . '\';');
1564
		// editor language file
1565
		if (!empty($txt['lang_locale']) && $txt['lang_locale'] != 'en_US')
1566
			loadJavaScriptFile($scripturl . '?action=loadeditorlocale', array('external' => true), 'sceditor_language');
1567
1568
		$context['shortcuts_text'] = $txt['shortcuts' . (!empty($context['drafts_save']) ? '_drafts' : '') . (stripos($_SERVER['HTTP_USER_AGENT'], 'Macintosh') !== false ? '_mac' : (isBrowser('is_firefox') ? '_firefox' : ''))];
1569
		$context['show_spellchecking'] = !empty($modSettings['enableSpellChecking']) && (function_exists('pspell_new') || (function_exists('enchant_broker_init') && ($txt['lang_character_set'] == 'UTF-8' || function_exists('iconv'))));
1570
		if ($context['show_spellchecking'])
1571
		{
1572
			loadJavaScriptFile('spellcheck.js', array('minimize' => true), 'smf_spellcheck');
1573
1574
			// Some hidden information is needed in order to make the spell checking work.
1575
			if (!isset($_REQUEST['xml']))
1576
				$context['insert_after_template'] .= '
1577
		<form name="spell_form" id="spell_form" method="post" accept-charset="' . $context['character_set'] . '" target="spellWindow" action="' . $scripturl . '?action=spellcheck">
1578
			<input type="hidden" name="spellstring" value="">
1579
		</form>';
1580
		}
1581
	}
1582
1583
	// Start off the editor...
1584
	$context['controls']['richedit'][$editorOptions['id']] = array(
1585
		'id' => $editorOptions['id'],
1586
		'value' => $editorOptions['value'],
1587
		'rich_value' => $editorOptions['value'], // 2.0 editor compatibility
1588
		'rich_active' => empty($modSettings['disable_wysiwyg']) && (!empty($options['wysiwyg_default']) || !empty($editorOptions['force_rich']) || !empty($_REQUEST[$editorOptions['id'] . '_mode'])),
1589
		'disable_smiley_box' => !empty($editorOptions['disable_smiley_box']),
1590
		'columns' => isset($editorOptions['columns']) ? $editorOptions['columns'] : 60,
1591
		'rows' => isset($editorOptions['rows']) ? $editorOptions['rows'] : 18,
1592
		'width' => isset($editorOptions['width']) ? $editorOptions['width'] : '70%',
1593
		'height' => isset($editorOptions['height']) ? $editorOptions['height'] : '250px',
1594
		'form' => isset($editorOptions['form']) ? $editorOptions['form'] : 'postmodify',
1595
		'bbc_level' => !empty($editorOptions['bbc_level']) ? $editorOptions['bbc_level'] : 'full',
1596
		'preview_type' => isset($editorOptions['preview_type']) ? (int) $editorOptions['preview_type'] : 1,
1597
		'labels' => !empty($editorOptions['labels']) ? $editorOptions['labels'] : array(),
1598
		'locale' => !empty($txt['lang_locale']) && substr($txt['lang_locale'], 0, 5) != 'en_US' ? $txt['lang_locale'] : '',
1599
		'required' => !empty($editorOptions['required']),
1600
	);
1601
1602
	if (empty($context['bbc_tags']))
1603
	{
1604
		// The below array makes it dead easy to add images to this control. Add it to the array and everything else is done for you!
1605
		// Note: 'before' and 'after' are deprecated as of SMF 2.1. Instead, use a separate JS file to configure the functionality of your toolbar buttons.
1606
		/*
1607
			array(
1608
				'code' => 'b', // Required
1609
				'description' => $editortxt['bold'], // Required
1610
				'image' => 'bold', // Optional
1611
				'before' => '[b]', // Deprecated
1612
				'after' => '[/b]', // Deprecated
1613
			),
1614
		*/
1615
		$context['bbc_tags'] = array();
1616
		$context['bbc_tags'][] = array(
1617
			array(
1618
				'code' => 'bold',
1619
				'description' => $editortxt['Bold'],
1620
			),
1621
			array(
1622
				'code' => 'italic',
1623
				'description' => $editortxt['Italic'],
1624
			),
1625
			array(
1626
				'code' => 'underline',
1627
				'description' => $editortxt['Underline']
1628
			),
1629
			array(
1630
				'code' => 'strike',
1631
				'description' => $editortxt['Strikethrough']
1632
			),
1633
			array(
1634
				'code' => 'superscript',
1635
				'description' => $editortxt['Superscript']
1636
			),
1637
			array(
1638
				'code' => 'subscript',
1639
				'description' => $editortxt['Subscript']
1640
			),
1641
			array(),
1642
			array(
1643
				'code' => 'pre',
1644
				'description' => $editortxt['Preformatted Text']
1645
			),
1646
			array(
1647
				'code' => 'left',
1648
				'description' => $editortxt['Align left']
1649
			),
1650
			array(
1651
				'code' => 'center',
1652
				'description' => $editortxt['Center']
1653
			),
1654
			array(
1655
				'code' => 'right',
1656
				'description' => $editortxt['Align right']
1657
			),
1658
			array(
1659
				'code' => 'justify',
1660
				'description' => $editortxt['Justify']
1661
			),
1662
			array(),
1663
			array(
1664
				'code' => 'font',
1665
				'description' => $editortxt['Font Name']
1666
			),
1667
			array(
1668
				'code' => 'size',
1669
				'description' => $editortxt['Font Size']
1670
			),
1671
			array(
1672
				'code' => 'color',
1673
				'description' => $editortxt['Font Color']
1674
			),
1675
		);
1676 View Code Duplication
		if (empty($modSettings['disable_wysiwyg']))
1677
		{
1678
			$context['bbc_tags'][count($context['bbc_tags']) - 1][] = array(
1679
				'code' => 'removeformat',
1680
				'description' => $editortxt['Remove Formatting'],
1681
			);
1682
		}
1683
		$context['bbc_tags'][] = array(
1684
			array(
1685
				'code' => 'floatleft',
1686
				'description' => $editortxt['Float left']
1687
			),
1688
			array(
1689
				'code' => 'floatright',
1690
				'description' => $editortxt['Float right']
1691
			),
1692
			array(),
1693
			array(
1694
				'code' => 'flash',
1695
				'description' => $editortxt['Flash']
1696
			),
1697
			array(
1698
				'code' => 'youtube',
1699
				'description' => $editortxt['Insert a YouTube video']
1700
			),
1701
			array(
1702
				'code' => 'image',
1703
				'description' => $editortxt['Insert an image']
1704
			),
1705
			array(
1706
				'code' => 'link',
1707
				'description' => $editortxt['Insert a link']
1708
			),
1709
			array(
1710
				'code' => 'email',
1711
				'description' => $editortxt['Insert an email']
1712
			),
1713
			array(),
1714
			array(
1715
				'code' => 'table',
1716
				'description' => $editortxt['Insert a table']
1717
			),
1718
			array(
1719
				'code' => 'code',
1720
				'description' => $editortxt['Code']
1721
			),
1722
			array(
1723
				'code' => 'quote',
1724
				'description' => $editortxt['Insert a Quote']
1725
			),
1726
			array(),
1727
			array(
1728
				'code' => 'bulletlist',
1729
				'description' => $editortxt['Bullet list']
1730
			),
1731
			array(
1732
				'code' => 'orderedlist',
1733
				'description' => $editortxt['Numbered list']
1734
			),
1735
			array(
1736
				'code' => 'horizontalrule',
1737
				'description' => $editortxt['Insert a horizontal rule']
1738
			),
1739
			array(),
1740
			array(
1741
				'code' => 'maximize',
1742
				'description' => $editortxt['Maximize']
1743
			),
1744
		);
1745 View Code Duplication
		if (empty($modSettings['disable_wysiwyg']))
1746
		{
1747
			$context['bbc_tags'][count($context['bbc_tags']) - 1][] = array(
1748
				'code' => 'source',
1749
				'description' => $editortxt['View source'],
1750
			);
1751
		}
1752
1753
		$editor_tag_map = array(
1754
			'b' => 'bold',
1755
			'i' => 'italic',
1756
			'u' => 'underline',
1757
			's' => 'strike',
1758
			'img' => 'image',
1759
			'url' => 'link',
1760
			'sup' => 'superscript',
1761
			'sub' => 'subscript',
1762
			'hr' => 'horizontalrule',
1763
		);
1764
1765
		// Allow mods to modify BBC buttons.
1766
		// Note: passing the array here is not necessary and is deprecated, but it is kept for backward compatibility with 2.0
1767
		call_integration_hook('integrate_bbc_buttons', array(&$context['bbc_tags'], &$editor_tag_map));
1768
1769
		// Generate a list of buttons that shouldn't be shown - this should be the fastest way to do this.
1770
		$disabled_tags = array();
1771
		if (!empty($modSettings['disabledBBC']))
1772
			$disabled_tags = explode(',', $modSettings['disabledBBC']);
1773
1774
		foreach ($disabled_tags as $tag)
1775
		{
1776
			$tag = trim($tag);
1777
1778
			if ($tag === 'list')
1779
			{
1780
				$context['disabled_tags']['bulletlist'] = true;
1781
				$context['disabled_tags']['orderedlist'] = true;
1782
			}
1783
1784
			foreach ($editor_tag_map as $thisTag => $tagNameBBC)
1785
				if ($tag === $thisTag)
1786
					$context['disabled_tags'][$tagNameBBC] = true;
1787
1788
			$context['disabled_tags'][$tag] = true;
1789
		}
1790
1791
		$bbcodes_styles = '';
1792
		$context['bbcodes_handlers'] = '';
1793
		$context['bbc_toolbar'] = array();
1794
1795
		foreach ($context['bbc_tags'] as $row => $tagRow)
1796
		{
1797
			if (!isset($context['bbc_toolbar'][$row]))
1798
				$context['bbc_toolbar'][$row] = array();
1799
1800
			$tagsRow = array();
1801
1802
			foreach ($tagRow as $tag)
1803
			{
1804
				if ((!empty($tag['code'])) && empty($context['disabled_tags'][$tag['code']]))
1805
				{
1806
					$tagsRow[] = $tag['code'];
1807
1808
					// If we have a custom button image, set it now.
1809
					if (isset($tag['image']))
1810
					{
1811
						$bbcodes_styles .= '
1812
						.sceditor-button-' . $tag['code'] . ' div {
1813
							background: url(\'' . $settings['default_theme_url'] . '/images/bbc/' . $tag['image'] . '.png\');
1814
						}';
1815
					}
1816
1817
					// Set the tooltip and possibly the command info
1818
					$context['bbcodes_handlers'] .= '
1819
						sceditor.command.set(' . JavaScriptEscape($tag['code']) . ', {
1820
							tooltip: ' . JavaScriptEscape(isset($tag['description']) ? $tag['description'] : $tag['code']);
1821
1822
					// Legacy support for 2.0 BBC mods
1823
					if (isset($tag['before']))
1824
					{
1825
						$context['bbcodes_handlers'] .= ',
1826
							exec: function () {
1827
								this.insert(' . JavaScriptEscape($tag['before']) . (isset($tag['after']) ? ', ' . JavaScriptEscape($tag['after']) : '') . ');
1828
							},
1829
							txtExec: [' . JavaScriptEscape($tag['before']) . (isset($tag['after']) ? ', ' . JavaScriptEscape($tag['after']) : '') . ']';
1830
					}
1831
1832
					$context['bbcodes_handlers'] .= '
1833
						});';
1834
				}
1835
				else
1836
				{
1837
					$context['bbc_toolbar'][$row][] = implode(',', $tagsRow);
1838
					$tagsRow = array();
1839
				}
1840
			}
1841
1842
			if (!empty($tagsRow))
1843
				$context['bbc_toolbar'][$row][] = implode(',', $tagsRow);
1844
		}
1845
1846
		if (!empty($bbcodes_styles))
1847
			addInlineCss($bbcodes_styles);
1848
	}
1849
1850
	// Initialize smiley array... if not loaded before.
1851
	if (empty($context['smileys']) && empty($editorOptions['disable_smiley_box']))
1852
	{
1853
		$context['smileys'] = array(
1854
			'postform' => array(),
1855
			'popup' => array(),
1856
		);
1857
1858
		// Load smileys - don't bother to run a query if we're not using the database's ones anyhow.
1859
		if (empty($modSettings['smiley_enable']) && $user_info['smiley_set'] != 'none')
1860
			$context['smileys']['postform'][] = array(
1861
				'smileys' => array(
1862
					array(
1863
						'code' => ':)',
1864
						'filename' => 'smiley.png',
1865
						'description' => $txt['icon_smiley'],
1866
					),
1867
					array(
1868
						'code' => ';)',
1869
						'filename' => 'wink.png',
1870
						'description' => $txt['icon_wink'],
1871
					),
1872
					array(
1873
						'code' => ':D',
1874
						'filename' => 'cheesy.png',
1875
						'description' => $txt['icon_cheesy'],
1876
					),
1877
					array(
1878
						'code' => ';D',
1879
						'filename' => 'grin.png',
1880
						'description' => $txt['icon_grin']
1881
					),
1882
					array(
1883
						'code' => '>:(',
1884
						'filename' => 'angry.png',
1885
						'description' => $txt['icon_angry'],
1886
					),
1887
					array(
1888
						'code' => ':(',
1889
						'filename' => 'sad.png',
1890
						'description' => $txt['icon_sad'],
1891
					),
1892
					array(
1893
						'code' => ':o',
1894
						'filename' => 'shocked.png',
1895
						'description' => $txt['icon_shocked'],
1896
					),
1897
					array(
1898
						'code' => '8)',
1899
						'filename' => 'cool.png',
1900
						'description' => $txt['icon_cool'],
1901
					),
1902
					array(
1903
						'code' => '???',
1904
						'filename' => 'huh.png',
1905
						'description' => $txt['icon_huh'],
1906
					),
1907
					array(
1908
						'code' => '::)',
1909
						'filename' => 'rolleyes.png',
1910
						'description' => $txt['icon_rolleyes'],
1911
					),
1912
					array(
1913
						'code' => ':P',
1914
						'filename' => 'tongue.png',
1915
						'description' => $txt['icon_tongue'],
1916
					),
1917
					array(
1918
						'code' => ':-[',
1919
						'filename' => 'embarrassed.png',
1920
						'description' => $txt['icon_embarrassed'],
1921
					),
1922
					array(
1923
						'code' => ':-X',
1924
						'filename' => 'lipsrsealed.png',
1925
						'description' => $txt['icon_lips'],
1926
					),
1927
					array(
1928
						'code' => ':-\\',
1929
						'filename' => 'undecided.png',
1930
						'description' => $txt['icon_undecided'],
1931
					),
1932
					array(
1933
						'code' => ':-*',
1934
						'filename' => 'kiss.png',
1935
						'description' => $txt['icon_kiss'],
1936
					),
1937
					array(
1938
						'code' => ':\'(',
1939
						'filename' => 'cry.png',
1940
						'description' => $txt['icon_cry'],
1941
						'isLast' => true,
1942
					),
1943
				),
1944
				'isLast' => true,
1945
			);
1946
		elseif ($user_info['smiley_set'] != 'none')
1947
		{
1948
			if (($temp = cache_get_data('posting_smileys', 480)) == null)
1949
			{
1950
				$request = $smcFunc['db_query']('', '
1951
					SELECT code, filename, description, smiley_row, hidden
1952
					FROM {db_prefix}smileys
1953
					WHERE hidden IN (0, 2)
1954
					ORDER BY smiley_row, smiley_order',
1955
					array(
1956
					)
1957
				);
1958
				while ($row = $smcFunc['db_fetch_assoc']($request))
1959
				{
1960
					$row['filename'] = $smcFunc['htmlspecialchars']($row['filename']);
1961
					$row['description'] = $smcFunc['htmlspecialchars']($row['description']);
1962
1963
					$context['smileys'][empty($row['hidden']) ? 'postform' : 'popup'][$row['smiley_row']]['smileys'][] = $row;
1964
				}
1965
				$smcFunc['db_free_result']($request);
1966
1967
				foreach ($context['smileys'] as $section => $smileyRows)
1968
				{
1969
					foreach ($smileyRows as $rowIndex => $smileys)
1970
						$context['smileys'][$section][$rowIndex]['smileys'][count($smileys['smileys']) - 1]['isLast'] = true;
1971
1972
					if (!empty($smileyRows))
1973
						$context['smileys'][$section][count($smileyRows) - 1]['isLast'] = true;
1974
				}
1975
1976
				cache_put_data('posting_smileys', $context['smileys'], 480);
1977
			}
1978
			else
1979
				$context['smileys'] = $temp;
1980
		}
1981
	}
1982
1983
	// Set a flag so the sub template knows what to do...
1984
	$context['show_bbc'] = !empty($modSettings['enableBBC']);
1985
1986
	// Set up the SCEditor options
1987
	$sce_options = array(
1988
		'style' => $settings['default_theme_url'] . '/css/jquery.sceditor.default.css',
1989
		'emoticonsCompat' => true,
1990
		'colors' => 'black,maroon,brown,green,navy,grey,red,orange,teal,blue,white,hotpink,yellow,limegreen,purple',
1991
		'format' => 'bbcode',
1992
		'plugins' => '',
1993
		'bbcodeTrim' => true,
1994
	);
1995
	if (!empty($context['controls']['richedit'][$editorOptions['id']]['locale']))
1996
		$sce_options['locale'] = $context['controls']['richedit'][$editorOptions['id']]['locale'];
1997
	if (!empty($context['right_to_left']))
1998
		$sce_options['rtl'] = true;
1999
	if ($editorOptions['id'] != 'quickReply')
2000
		$sce_options['autofocus'] = true;
2001
2002
	$sce_options['emoticons'] = array();
2003
	$sce_options['emoticonsDescriptions'] = array();
2004
	$sce_options['emoticonsEnabled'] = false;
2005
	if ((!empty($context['smileys']['postform']) || !empty($context['smileys']['popup'])) && !$context['controls']['richedit'][$editorOptions['id']]['disable_smiley_box'])
2006
	{
2007
		$sce_options['emoticonsEnabled'] = true;
2008
		$sce_options['emoticons']['dropdown'] = array();
2009
		$sce_options['emoticons']['popup'] = array();
2010
2011
		$countLocations = count($context['smileys']);
2012
		foreach ($context['smileys'] as $location => $smileyRows)
2013
		{
2014
			$countLocations--;
2015
2016
			unset($smiley_location);
2017
			if ($location == 'postform')
2018
				$smiley_location = &$sce_options['emoticons']['dropdown'];
2019
			elseif ($location == 'popup')
2020
				$smiley_location = &$sce_options['emoticons']['popup'];
2021
2022
			$numRows = count($smileyRows);
2023
2024
			// This is needed because otherwise the editor will remove all the duplicate (empty) keys and leave only 1 additional line
2025
			$emptyPlaceholder = 0;
2026
			foreach ($smileyRows as $smileyRow)
2027
			{
2028
				foreach ($smileyRow['smileys'] as $smiley)
2029
				{
2030
					$smiley_location[$smiley['code']] = $settings['smileys_url'] . '/' . $smiley['filename'];
0 ignored issues
show
Bug introduced by
The variable $smiley_location does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2031
					$sce_options['emoticonsDescriptions'][$smiley['code']] = $smiley['description'];
2032
				}
2033
2034
				if (empty($smileyRow['isLast']) && $numRows != 1)
2035
					$smiley_location['-' . $emptyPlaceholder++] = '';
2036
			}
2037
		}
2038
	}
2039
2040
	$sce_options['toolbar'] = '';
2041
	if ($context['show_bbc'])
2042
	{
2043
		$count_tags = count($context['bbc_tags']);
2044
		foreach ($context['bbc_toolbar'] as $i => $buttonRow)
2045
		{
2046
			$sce_options['toolbar'] .= implode('|', $buttonRow);
2047
2048
			$count_tags--;
2049
2050
			if (!empty($count_tags))
2051
				$sce_options['toolbar'] .= '||';
2052
		}
2053
	}
2054
2055
	// Allow mods to change $sce_options. Usful if, e.g., a mod wants to add an SCEditor plugin.
2056
	call_integration_hook('integrate_sceditor_options', array(&$sce_options));
2057
2058
	$context['controls']['richedit'][$editorOptions['id']]['sce_options'] = $sce_options;
2059
}
2060
2061
/**
2062
 * Create a anti-bot verification control?
2063
 * @param array &$verificationOptions Options for the verification control
2064
 * @param bool $do_test Whether to check to see if the user entered the code correctly
2065
 * @return bool|array False if there's nothing to show, true if everything went well or an array containing error indicators if the test failed
2066
 */
2067
function create_control_verification(&$verificationOptions, $do_test = false)
2068
{
2069
	global $modSettings, $smcFunc;
2070
	global $context, $user_info, $scripturl, $language;
2071
2072
	// First verification means we need to set up some bits...
2073
	if (empty($context['controls']['verification']))
2074
	{
2075
		// The template
2076
		loadTemplate('GenericControls');
2077
2078
		// Some javascript ma'am?
2079
		if (!empty($verificationOptions['override_visual']) || (!empty($modSettings['visual_verification_type']) && !isset($verificationOptions['override_visual'])))
2080
			loadJavaScriptFile('captcha.js', array('minimize' => true), 'smf_captcha');
2081
2082
		$context['use_graphic_library'] = in_array('gd', get_loaded_extensions());
2083
2084
		// Skip I, J, L, O, Q, S and Z.
2085
		$context['standard_captcha_range'] = array_merge(range('A', 'H'), array('K', 'M', 'N', 'P', 'R'), range('T', 'Y'));
2086
	}
2087
2088
	// Always have an ID.
2089
	assert(isset($verificationOptions['id']));
2090
	$isNew = !isset($context['controls']['verification'][$verificationOptions['id']]);
2091
2092
	// Log this into our collection.
2093
	if ($isNew)
2094
		$context['controls']['verification'][$verificationOptions['id']] = array(
2095
			'id' => $verificationOptions['id'],
2096
			'empty_field' => empty($verificationOptions['no_empty_field']),
2097
			'show_visual' => !empty($verificationOptions['override_visual']) || (!empty($modSettings['visual_verification_type']) && !isset($verificationOptions['override_visual'])),
2098
			'number_questions' => isset($verificationOptions['override_qs']) ? $verificationOptions['override_qs'] : (!empty($modSettings['qa_verification_number']) ? $modSettings['qa_verification_number'] : 0),
2099
			'max_errors' => isset($verificationOptions['max_errors']) ? $verificationOptions['max_errors'] : 3,
2100
			'image_href' => $scripturl . '?action=verificationcode;vid=' . $verificationOptions['id'] . ';rand=' . md5(mt_rand()),
2101
			'text_value' => '',
2102
			'questions' => array(),
2103
			'can_recaptcha' => !empty($modSettings['recaptcha_enabled']) && !empty($modSettings['recaptcha_site_key']) && !empty($modSettings['recaptcha_secret_key']),
2104
		);
2105
	$thisVerification = &$context['controls']['verification'][$verificationOptions['id']];
2106
2107
	// Is there actually going to be anything?
2108
	if (empty($thisVerification['show_visual']) && empty($thisVerification['number_questions']) && empty($thisVerification['can_recaptcha']))
2109
		return false;
2110
	elseif (!$isNew && !$do_test)
2111
		return true;
2112
2113
	// Sanitize reCAPTCHA fields?
2114
	if ($thisVerification['can_recaptcha'])
2115
	{
2116
		// Only allow 40 alphanumeric, underscore and dash characters.
2117
		$thisVerification['recaptcha_site_key'] = preg_replace('/(0-9a-zA-Z_){40}/', '$1', $modSettings['recaptcha_site_key']);
2118
2119
		// Light or dark theme...
2120
		$thisVerification['recaptcha_theme'] = preg_replace('/(light|dark)/', '$1', $modSettings['recaptcha_theme']);
2121
	}
2122
2123
	// Add javascript for the object.
2124
	if ($context['controls']['verification'][$verificationOptions['id']]['show_visual'])
2125
		$context['insert_after_template'] .= '
2126
			<script>
2127
				var verification' . $verificationOptions['id'] . 'Handle = new smfCaptcha("' . $thisVerification['image_href'] . '", "' . $verificationOptions['id'] . '", ' . ($context['use_graphic_library'] ? 1 : 0) . ');
2128
			</script>';
2129
2130
	// If we want questions do we have a cache of all the IDs?
2131
	if (!empty($thisVerification['number_questions']) && empty($modSettings['question_id_cache']))
2132
	{
2133
		if (($modSettings['question_id_cache'] = cache_get_data('verificationQuestions', 300)) == null)
2134
		{
2135
			$request = $smcFunc['db_query']('', '
2136
				SELECT id_question, lngfile, question, answers
2137
				FROM {db_prefix}qanda',
2138
				array()
2139
			);
2140
			$modSettings['question_id_cache'] = array(
2141
				'questions' => array(),
2142
				'langs' => array(),
2143
			);
2144
			// This is like Captain Kirk climbing a mountain in some ways. This is L's fault, mkay? :P
2145
			while ($row = $smcFunc['db_fetch_assoc']($request))
2146
			{
2147
				$id_question = $row['id_question'];
2148
				unset ($row['id_question']);
2149
				// Make them all lowercase. We can't directly use $smcFunc['strtolower'] with array_walk, so do it manually, eh?
2150
				$row['answers'] = $smcFunc['json_decode']($row['answers'], true);
2151
				foreach ($row['answers'] as $k => $v)
2152
					$row['answers'][$k] = $smcFunc['strtolower']($v);
2153
2154
				$modSettings['question_id_cache']['questions'][$id_question] = $row;
2155
				$modSettings['question_id_cache']['langs'][$row['lngfile']][] = $id_question;
2156
			}
2157
			$smcFunc['db_free_result']($request);
2158
2159
			cache_put_data('verificationQuestions', $modSettings['question_id_cache'], 300);
2160
		}
2161
	}
2162
2163
	if (!isset($_SESSION[$verificationOptions['id'] . '_vv']))
2164
		$_SESSION[$verificationOptions['id'] . '_vv'] = array();
2165
2166
	// Do we need to refresh the verification?
2167
	if (!$do_test && (!empty($_SESSION[$verificationOptions['id'] . '_vv']['did_pass']) || empty($_SESSION[$verificationOptions['id'] . '_vv']['count']) || $_SESSION[$verificationOptions['id'] . '_vv']['count'] > 3) && empty($verificationOptions['dont_refresh']))
2168
		$force_refresh = true;
2169
	else
2170
		$force_refresh = false;
2171
2172
	// This can also force a fresh, although unlikely.
2173 View Code Duplication
	if (($thisVerification['show_visual'] && empty($_SESSION[$verificationOptions['id'] . '_vv']['code'])) || ($thisVerification['number_questions'] && empty($_SESSION[$verificationOptions['id'] . '_vv']['q'])))
2174
		$force_refresh = true;
2175
2176
	$verification_errors = array();
2177
	// Start with any testing.
2178
	if ($do_test)
2179
	{
2180
		// This cannot happen!
2181
		if (!isset($_SESSION[$verificationOptions['id'] . '_vv']['count']))
2182
			fatal_lang_error('no_access', false);
2183
		// ... nor this!
2184
		if ($thisVerification['number_questions'] && (!isset($_SESSION[$verificationOptions['id'] . '_vv']['q']) || !isset($_REQUEST[$verificationOptions['id'] . '_vv']['q'])))
2185
			fatal_lang_error('no_access', false);
2186
		// Hmm, it's requested but not actually declared. This shouldn't happen.
2187
		if ($thisVerification['empty_field'] && empty($_SESSION[$verificationOptions['id'] . '_vv']['empty_field']))
2188
			fatal_lang_error('no_access', false);
2189
		// While we're here, did the user do something bad?
2190 View Code Duplication
		if ($thisVerification['empty_field'] && !empty($_SESSION[$verificationOptions['id'] . '_vv']['empty_field']) && !empty($_REQUEST[$_SESSION[$verificationOptions['id'] . '_vv']['empty_field']]))
2191
			$verification_errors[] = 'wrong_verification_answer';
2192
2193
		if ($thisVerification['can_recaptcha'])
2194
		{
2195
			$reCaptcha = new \ReCaptcha\ReCaptcha($modSettings['recaptcha_secret_key'], new \ReCaptcha\RequestMethod\SocketPost());
2196
2197
			// Was there a reCAPTCHA response?
2198
			if (isset($_POST['g-recaptcha-response']))
2199
			{
2200
				$resp = $reCaptcha->verify($_POST['g-recaptcha-response'], $user_info['ip']);
2201
2202
				if (!$resp->isSuccess())
2203
					$verification_errors[] = 'wrong_verification_code';
2204
			}
2205
			else
2206
				$verification_errors[] = 'wrong_verification_code';
2207
		}
2208
		if ($thisVerification['show_visual'] && (empty($_REQUEST[$verificationOptions['id'] . '_vv']['code']) || empty($_SESSION[$verificationOptions['id'] . '_vv']['code']) || strtoupper($_REQUEST[$verificationOptions['id'] . '_vv']['code']) !== $_SESSION[$verificationOptions['id'] . '_vv']['code']))
2209
			$verification_errors[] = 'wrong_verification_code';
2210
		if ($thisVerification['number_questions'])
2211
		{
2212
			$incorrectQuestions = array();
2213
			foreach ($_SESSION[$verificationOptions['id'] . '_vv']['q'] as $q)
2214
			{
2215
				// We don't have this question any more, thus no answers.
2216
				if (!isset($modSettings['question_id_cache']['questions'][$q]))
2217
					continue;
2218
				// This is quite complex. We have our question but it might have multiple answers.
2219
				// First, did they actually answer this question?
2220
				if (!isset($_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q]) || trim($_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q]) == '')
2221
				{
2222
					$incorrectQuestions[] = $q;
2223
					continue;
2224
				}
2225
				// Second, is their answer in the list of possible answers?
2226
				else
2227
				{
2228
					$given_answer = trim($smcFunc['htmlspecialchars'](strtolower($_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q])));
2229
					if (!in_array($given_answer, $modSettings['question_id_cache']['questions'][$q]['answers']))
2230
						$incorrectQuestions[] = $q;
2231
				}
2232
			}
2233
2234
			if (!empty($incorrectQuestions))
2235
				$verification_errors[] = 'wrong_verification_answer';
2236
		}
2237
	}
2238
2239
	// Any errors means we refresh potentially.
2240
	if (!empty($verification_errors))
2241
	{
2242
		if (empty($_SESSION[$verificationOptions['id'] . '_vv']['errors']))
2243
			$_SESSION[$verificationOptions['id'] . '_vv']['errors'] = 0;
2244
		// Too many errors?
2245
		elseif ($_SESSION[$verificationOptions['id'] . '_vv']['errors'] > $thisVerification['max_errors'])
2246
			$force_refresh = true;
2247
2248
		// Keep a track of these.
2249
		$_SESSION[$verificationOptions['id'] . '_vv']['errors']++;
2250
	}
2251
2252
	// Are we refreshing then?
2253
	if ($force_refresh)
2254
	{
2255
		// Assume nothing went before.
2256
		$_SESSION[$verificationOptions['id'] . '_vv']['count'] = 0;
2257
		$_SESSION[$verificationOptions['id'] . '_vv']['errors'] = 0;
2258
		$_SESSION[$verificationOptions['id'] . '_vv']['did_pass'] = false;
2259
		$_SESSION[$verificationOptions['id'] . '_vv']['q'] = array();
2260
		$_SESSION[$verificationOptions['id'] . '_vv']['code'] = '';
2261
2262
		// Make our magic empty field.
2263
		if ($thisVerification['empty_field'])
2264
		{
2265
			// We're building a field that lives in the template, that we hope to be empty later. But at least we give it a believable name.
2266
			$terms = array('gadget', 'device', 'uid', 'gid', 'guid', 'uuid', 'unique', 'identifier');
2267
			$second_terms = array('hash', 'cipher', 'code', 'key', 'unlock', 'bit', 'value');
2268
			$start = mt_rand(0, 27);
2269
			$hash = substr(md5(time()), $start, 4);
2270
			$_SESSION[$verificationOptions['id'] . '_vv']['empty_field'] = $terms[array_rand($terms)] . '-' . $second_terms[array_rand($second_terms)] . '-' . $hash;
2271
		}
2272
2273
		// Generating a new image.
2274
		if ($thisVerification['show_visual'])
2275
		{
2276
			// Are we overriding the range?
2277
			$character_range = !empty($verificationOptions['override_range']) ? $verificationOptions['override_range'] : $context['standard_captcha_range'];
2278
2279
			for ($i = 0; $i < 6; $i++)
2280
				$_SESSION[$verificationOptions['id'] . '_vv']['code'] .= $character_range[array_rand($character_range)];
2281
		}
2282
2283
		// Getting some new questions?
2284
		if ($thisVerification['number_questions'])
2285
		{
2286
			// Attempt to try the current page's language, followed by the user's preference, followed by the site default.
2287
			$possible_langs = array();
2288
			if (isset($_SESSION['language']))
2289
				$possible_langs[] = strtr($_SESSION['language'], array('-utf8' => ''));
0 ignored issues
show
Documentation introduced by
$_SESSION['language'] is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2290
			if (!empty($user_info['language']));
2291
			$possible_langs[] = $user_info['language'];
2292
			$possible_langs[] = $language;
2293
2294
			$questionIDs = array();
2295
			foreach ($possible_langs as $lang)
2296
			{
2297
				$lang = strtr($lang, array('-utf8' => ''));
2298
				if (isset($modSettings['question_id_cache']['langs'][$lang]))
2299
				{
2300
					// If we find questions for this, grab the ids from this language's ones, randomize the array and take just the number we need.
2301
					$questionIDs = $modSettings['question_id_cache']['langs'][$lang];
2302
					shuffle($questionIDs);
2303
					$questionIDs = array_slice($questionIDs, 0, $thisVerification['number_questions']);
2304
					break;
2305
				}
2306
			}
2307
		}
2308
	}
2309
	else
2310
	{
2311
		// Same questions as before.
2312
		$questionIDs = !empty($_SESSION[$verificationOptions['id'] . '_vv']['q']) ? $_SESSION[$verificationOptions['id'] . '_vv']['q'] : array();
2313
		$thisVerification['text_value'] = !empty($_REQUEST[$verificationOptions['id'] . '_vv']['code']) ? $smcFunc['htmlspecialchars']($_REQUEST[$verificationOptions['id'] . '_vv']['code']) : '';
2314
	}
2315
2316
	// If we do have an empty field, it would be nice to hide it from legitimate users who shouldn't be populating it anyway.
2317
	if (!empty($_SESSION[$verificationOptions['id'] . '_vv']['empty_field']))
2318
	{
2319
		if (!isset($context['html_headers']))
2320
			$context['html_headers'] = '';
2321
		$context['html_headers'] .= '<style>.vv_special { display:none; }</style>';
2322
	}
2323
2324
	// Have we got some questions to load?
2325
	if (!empty($questionIDs))
2326
	{
2327
		$_SESSION[$verificationOptions['id'] . '_vv']['q'] = array();
2328
		foreach ($questionIDs as $q)
2329
		{
2330
			// Bit of a shortcut this.
2331
			$row = &$modSettings['question_id_cache']['questions'][$q];
2332
			$thisVerification['questions'][] = array(
2333
				'id' => $q,
2334
				'q' => parse_bbc($row['question']),
2335
				'is_error' => !empty($incorrectQuestions) && in_array($q, $incorrectQuestions),
2336
				// Remember a previous submission?
2337
				'a' => isset($_REQUEST[$verificationOptions['id'] . '_vv'], $_REQUEST[$verificationOptions['id'] . '_vv']['q'], $_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q]) ? $smcFunc['htmlspecialchars']($_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q]) : '',
2338
			);
2339
			$_SESSION[$verificationOptions['id'] . '_vv']['q'][] = $q;
2340
		}
2341
	}
2342
2343
	$_SESSION[$verificationOptions['id'] . '_vv']['count'] = empty($_SESSION[$verificationOptions['id'] . '_vv']['count']) ? 1 : $_SESSION[$verificationOptions['id'] . '_vv']['count'] + 1;
2344
2345
	// Return errors if we have them.
2346
	if (!empty($verification_errors))
2347
		return $verification_errors;
2348
	// If we had a test that one, make a note.
2349
	elseif ($do_test)
2350
		$_SESSION[$verificationOptions['id'] . '_vv']['did_pass'] = true;
2351
2352
	// Say that everything went well chaps.
2353
	return true;
2354
}
2355
2356
/**
2357
 * This keeps track of all registered handling functions for auto suggest functionality and passes execution to them.
2358
 * @param bool $checkRegistered If set to something other than null, checks whether the callback function is registered
0 ignored issues
show
Documentation introduced by
Should the type for parameter $checkRegistered not be boolean|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
2359
 * @return void|bool Returns whether the callback function is registered if $checkRegistered isn't null
2360
 */
2361
function AutoSuggestHandler($checkRegistered = null)
2362
{
2363
	global $smcFunc, $context;
2364
2365
	// These are all registered types.
2366
	$searchTypes = array(
2367
		'member' => 'Member',
2368
		'membergroups' => 'MemberGroups',
2369
		'versions' => 'SMFVersions',
2370
	);
2371
2372
	call_integration_hook('integrate_autosuggest', array(&$searchTypes));
2373
2374
	// If we're just checking the callback function is registered return true or false.
2375
	if ($checkRegistered != null)
2376
		return isset($searchTypes[$checkRegistered]) && function_exists('AutoSuggest_Search_' . $checkRegistered);
2377
2378
	checkSession('get');
2379
	loadTemplate('Xml');
2380
2381
	// Any parameters?
2382
	$context['search_param'] = isset($_REQUEST['search_param']) ? $smcFunc['json_decode'](base64_decode($_REQUEST['search_param']), true) : array();
2383
2384
	if (isset($_REQUEST['suggest_type'], $_REQUEST['search']) && isset($searchTypes[$_REQUEST['suggest_type']]))
2385
	{
2386
		$function = 'AutoSuggest_Search_' . $searchTypes[$_REQUEST['suggest_type']];
2387
		$context['sub_template'] = 'generic_xml';
2388
		$context['xml_data'] = $function();
2389
	}
2390
}
2391
2392
/**
2393
 * Search for a member - by real_name or member_name by default.
2394
 *
2395
 * @return array An array of information for displaying the suggestions
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,array<string,string|array>>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
2396
 */
2397
function AutoSuggest_Search_Member()
2398
{
2399
	global $user_info, $smcFunc, $context;
2400
2401
	$_REQUEST['search'] = trim($smcFunc['strtolower']($_REQUEST['search'])) . '*';
2402
	$_REQUEST['search'] = strtr($_REQUEST['search'], array('%' => '\%', '_' => '\_', '*' => '%', '?' => '_', '&#038;' => '&amp;'));
2403
2404
	// Find the member.
2405
	$request = $smcFunc['db_query']('', '
2406
		SELECT id_member, real_name
2407
		FROM {db_prefix}members
2408
		WHERE {raw:real_name} LIKE {string:search}' . (!empty($context['search_param']['buddies']) ? '
2409
			AND id_member IN ({array_int:buddy_list})' : '') . '
2410
			AND is_activated IN (1, 11)
2411
		LIMIT ' . ($smcFunc['strlen']($_REQUEST['search']) <= 2 ? '100' : '800'),
2412
		array(
2413
			'real_name' => $smcFunc['db_case_sensitive'] ? 'LOWER(real_name)' : 'real_name',
2414
			'buddy_list' => $user_info['buddies'],
2415
			'search' => $_REQUEST['search'],
2416
		)
2417
	);
2418
	$xml_data = array(
2419
		'items' => array(
2420
			'identifier' => 'item',
2421
			'children' => array(),
2422
		),
2423
	);
2424 View Code Duplication
	while ($row = $smcFunc['db_fetch_assoc']($request))
2425
	{
2426
		$row['real_name'] = strtr($row['real_name'], array('&amp;' => '&#038;', '&lt;' => '&#060;', '&gt;' => '&#062;', '&quot;' => '&#034;'));
2427
2428
		$xml_data['items']['children'][] = array(
2429
			'attributes' => array(
2430
				'id' => $row['id_member'],
2431
			),
2432
			'value' => $row['real_name'],
2433
		);
2434
	}
2435
	$smcFunc['db_free_result']($request);
2436
2437
	return $xml_data;
2438
}
2439
2440
/**
2441
 * Search for a membergroup by name
2442
 *
2443
 * @return array An array of information for displaying the suggestions
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,array<string,string|array>>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
2444
 */
2445
function AutoSuggest_Search_MemberGroups()
2446
{
2447
	global $smcFunc;
2448
2449
	$_REQUEST['search'] = trim($smcFunc['strtolower']($_REQUEST['search'])) . '*';
2450
	$_REQUEST['search'] = strtr($_REQUEST['search'], array('%' => '\%', '_' => '\_', '*' => '%', '?' => '_', '&#038;' => '&amp;'));
2451
2452
	// Find the group.
2453
	// Only return groups which are not post-based and not "Hidden", but not the "Administrators" or "Moderators" groups.
2454
	$request = $smcFunc['db_query']('', '
2455
		SELECT id_group, group_name
2456
		FROM {db_prefix}membergroups
2457
		WHERE {raw:group_name} LIKE {string:search}
2458
			AND min_posts = {int:min_posts}
2459
			AND id_group NOT IN ({array_int:invalid_groups})
2460
			AND hidden != {int:hidden}
2461
		',
2462
		array(
2463
			'group_name' => $smcFunc['db_case_sensitive'] ? 'LOWER(group_name}' : 'group_name',
2464
			'min_posts' => -1,
2465
			'invalid_groups' => array(1, 3),
2466
			'hidden' => 2,
2467
			'search' => $_REQUEST['search'],
2468
		)
2469
	);
2470
	$xml_data = array(
2471
		'items' => array(
2472
			'identifier' => 'item',
2473
			'children' => array(),
2474
		),
2475
	);
2476 View Code Duplication
	while ($row = $smcFunc['db_fetch_assoc']($request))
2477
	{
2478
		$row['group_name'] = strtr($row['group_name'], array('&amp;' => '&#038;', '&lt;' => '&#060;', '&gt;' => '&#062;', '&quot;' => '&#034;'));
2479
2480
		$xml_data['items']['children'][] = array(
2481
			'attributes' => array(
2482
				'id' => $row['id_group'],
2483
			),
2484
			'value' => $row['group_name'],
2485
		);
2486
	}
2487
	$smcFunc['db_free_result']($request);
2488
2489
	return $xml_data;
2490
}
2491
2492
/**
2493
 * Provides a list of possible SMF versions to use in emulation
2494
 *
2495
 * @return array An array of data for displaying the suggestions
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,array<string,string|array>>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
2496
 */
2497
function AutoSuggest_Search_SMFVersions()
2498
{
2499
	global $smcFunc;
2500
2501
	$xml_data = array(
2502
		'items' => array(
2503
			'identifier' => 'item',
2504
			'children' => array(),
2505
		),
2506
	);
2507
2508
	// First try and get it from the database.
2509
	$versions = array();
2510
	$request = $smcFunc['db_query']('', '
2511
		SELECT data
2512
		FROM {db_prefix}admin_info_files
2513
		WHERE filename = {string:latest_versions}
2514
			AND path = {string:path}',
2515
		array(
2516
			'latest_versions' => 'latest-versions.txt',
2517
			'path' => '/smf/',
2518
		)
2519
	);
2520
	if (($smcFunc['db_num_rows']($request) > 0) && ($row = $smcFunc['db_fetch_assoc']($request)) && !empty($row['data']))
2521
	{
2522
		// The file can be either Windows or Linux line endings, but let's ensure we clean it as best we can.
2523
		$possible_versions = explode("\n", $row['data']);
2524
		foreach ($possible_versions as $ver)
2525
		{
2526
			$ver = trim($ver);
2527
			if (strpos($ver, 'SMF') === 0)
2528
				$versions[] = $ver;
2529
		}
2530
	}
2531
	$smcFunc['db_free_result']($request);
2532
2533
	// Just in case we don't have ANYthing.
2534
	if (empty($versions))
2535
		$versions = array('SMF 2.0');
2536
2537
	foreach ($versions as $id => $version)
2538
		if (strpos($version, strtoupper($_REQUEST['search'])) !== false)
2539
			$xml_data['items']['children'][] = array(
2540
				'attributes' => array(
2541
					'id' => $id,
2542
				),
2543
				'value' => $version,
2544
			);
2545
2546
	return $xml_data;
2547
}
2548
2549
?>