Failed Conditions
Branch release-2.1 (4e22cf)
by Rick
06:39
created

Subs-Editor.php ➔ create_control_richedit()   F

Complexity

Conditions 65
Paths > 20000

Size

Total Lines 440
Code Lines 286

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 65
eloc 286
nc 429496.7295
nop 1
dl 0
loc 440
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file 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 2017 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
 * !!!Compatibility!!!
22
 * Since we changed the editor we don't need it any more, but let's keep it if any mod wants to use it
23
 * Convert only the BBC that can be edited in HTML mode for the editor.
24
 *
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 ?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
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)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $m. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
81
		{
82
			return '<' . stripslashes($m[1]) . 'alt="" title="" onresizestart="return false;" id="smiley_' . $i++ . '_' . $m[2] . '" style="padding: 0 3px 0 3px;">';
0 ignored issues
show
Coding Style introduced by
Increment and decrement operators must be bracketed when used in string concatenation
Loading history...
83
		}, $text);
84
85
	return $text;
86
}
87
88
/**
89
 * !!!Compatibility!!!
90
 * This is no more needed, but to avoid break mods let's keep it
91
 * Run it it shouldn't even hurt either, so let's not bother remove it
92
 *
93
 * The harder one - wysiwyg to BBC!
94
 *
95
 * @param string $text Text containing HTML
96
 * @return string The text with html converted to bbc
97
 */
98
function html_to_bbc($text)
99
{
100
	global $modSettings, $smcFunc, $scripturl, $context;
101
102
	// Replace newlines with spaces, as that's how browsers usually interpret them.
103
	$text = preg_replace("~\s*[\r\n]+\s*~", ' ', $text);
104
105
	// Though some of us love paragraphs, the parser will do better with breaks.
106
	$text = preg_replace('~</p>\s*?<p~i', '</p><br><p', $text);
107
	$text = preg_replace('~</p>\s*(?!<)~i', '</p><br>', $text);
108
109
	// Safari/webkit wraps lines in Wysiwyg in <div>'s.
110
	if (isBrowser('webkit'))
111
		$text = preg_replace(array('~<div(?:\s(?:[^<>]*?))?' . '>~i', '</div>'), array('<br>', ''), $text);
112
113
	// If there's a trailing break get rid of it - Firefox tends to add one.
114
	$text = preg_replace('~<br\s?/?' . '>$~i', '', $text);
115
116
	// Remove any formatting within code tags.
117
	if (strpos($text, '[code') !== false)
118
	{
119
		$text = preg_replace('~<br\s?/?' . '>~i', '#smf_br_spec_grudge_cool!#', $text);
120
		$parts = preg_split('~(\[/code\]|\[code(?:=[^\]]+)?\])~i', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
121
122
		// Only mess with stuff outside [code] tags.
123
		for ($i = 0, $n = count($parts); $i < $n; $i++)
124
		{
125
			// Value of 2 means we're inside the tag.
126
			if ($i % 4 == 2)
127
				$parts[$i] = strip_tags($parts[$i]);
128
		}
129
130
		$text = strtr(implode('', $parts), array('#smf_br_spec_grudge_cool!#' => '<br>'));
131
	}
132
133
	// Remove scripts, style and comment blocks.
134
	$text = preg_replace('~<script[^>]*[^/]?' . '>.*?</script>~i', '', $text);
135
	$text = preg_replace('~<style[^>]*[^/]?' . '>.*?</style>~i', '', $text);
136
	$text = preg_replace('~\\<\\!--.*?-->~i', '', $text);
137
	$text = preg_replace('~\\<\\!\\[CDATA\\[.*?\\]\\]\\>~i', '', $text);
138
139
	// Do the smileys ultra first!
140
	preg_match_all('~<img\s+[^<>]*?id="*smiley_\d+_([^<>]+?)[\s"/>]\s*[^<>]*?/*>(?:\s)?~i', $text, $matches);
141
	if (!empty($matches[0]))
142
	{
143
		// Easy if it's not custom.
144
		if (empty($modSettings['smiley_enable']))
145
		{
146
			$smileysfrom = array('>:D', ':D', '::)', '>:(', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', '0:)');
147
			$smileysto = array('evil.gif', 'cheesy.gif', 'rolleyes.gif', 'angry.gif', 'smiley.gif', 'wink.gif', 'grin.gif', 'sad.gif', 'shocked.gif', 'cool.gif', 'tongue.gif', 'huh.gif', 'embarrassed.gif', 'lipsrsealed.gif', 'kiss.gif', 'cry.gif', 'undecided.gif', 'azn.gif', 'afro.gif', 'police.gif', 'angel.gif');
148
149
			foreach ($matches[1] as $k => $file)
150
			{
151
				$found = array_search($file, $smileysto);
152
				// Note the weirdness here is to stop double spaces between smileys.
153
				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...
154
					$matches[1][$k] = '-[]-smf_smily_start#|#' . $smcFunc['htmlspecialchars']($smileysfrom[$found]) . '-[]-smf_smily_end#|#';
155
				else
156
					$matches[1][$k] = '';
157
			}
158
		}
159
		else
160
		{
161
			// Load all the smileys.
162
			$names = array();
163
			foreach ($matches[1] as $file)
164
				$names[] = $file;
165
			$names = array_unique($names);
166
167
			if (!empty($names))
168
			{
169
				$request = $smcFunc['db_query']('', '
170
					SELECT code, filename
171
					FROM {db_prefix}smileys
172
					WHERE filename IN ({array_string:smiley_filenames})',
173
					array(
174
						'smiley_filenames' => $names,
175
					)
176
				);
177
				$mappings = array();
178
				while ($row = $smcFunc['db_fetch_assoc']($request))
179
					$mappings[$row['filename']] = $smcFunc['htmlspecialchars']($row['code']);
180
				$smcFunc['db_free_result']($request);
181
182
				foreach ($matches[1] as $k => $file)
183
					if (isset($mappings[$file]))
184
						$matches[1][$k] = '-[]-smf_smily_start#|#' . $mappings[$file] . '-[]-smf_smily_end#|#';
185
			}
186
		}
187
188
		// Replace the tags!
189
		$text = str_replace($matches[0], $matches[1], $text);
190
191
		// Now sort out spaces
192
		$text = str_replace(array('-[]-smf_smily_end#|#-[]-smf_smily_start#|#', '-[]-smf_smily_end#|#', '-[]-smf_smily_start#|#'), ' ', $text);
193
	}
194
195
	// Only try to buy more time if the client didn't quit.
196
	if (connection_aborted() && $context['server']['is_apache'])
197
		@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...
198
199
	$parts = preg_split('~(<[A-Za-z]+\s*[^<>]*?style="?[^<>"]+"?[^<>]*?(?:/?)>|</[A-Za-z]+>)~', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
200
	$replacement = '';
201
	$stack = array();
202
203
	foreach ($parts as $part)
204
	{
205
		if (preg_match('~(<([A-Za-z]+)\s*[^<>]*?)style="?([^<>"]+)"?([^<>]*?(/?)>)~', $part, $matches) === 1)
206
		{
207
			// If it's being closed instantly, we can't deal with it...yet.
208
			if ($matches[5] === '/')
209
				continue;
210
			else
211
			{
212
				// Get an array of styles that apply to this element. (The strtr is there to combat HTML generated by Word.)
213
				$styles = explode(';', strtr($matches[3], array('&quot;' => '')));
214
				$curElement = $matches[2];
215
				$precedingStyle = $matches[1];
216
				$afterStyle = $matches[4];
217
				$curCloseTags = '';
218
				$extra_attr = '';
219
220
				foreach ($styles as $type_value_pair)
221
				{
222
					// Remove spaces and convert uppercase letters.
223
					$clean_type_value_pair = strtolower(strtr(trim($type_value_pair), '=', ':'));
224
225
					// Something like 'font-weight: bold' is expected here.
226
					if (strpos($clean_type_value_pair, ':') === false)
227
						continue;
228
229
					// Capture the elements of a single style item (e.g. 'font-weight' and 'bold').
230
					list ($style_type, $style_value) = explode(':', $type_value_pair);
231
232
					$style_value = trim($style_value);
233
234
					switch (trim($style_type))
235
					{
236
						case 'font-weight':
237
							if ($style_value === 'bold')
238
							{
239
								$curCloseTags .= '[/b]';
240
								$replacement .= '[b]';
241
							}
242
						break;
243
244
						case 'text-decoration':
245
							if ($style_value == 'underline')
246
							{
247
								$curCloseTags .= '[/u]';
248
								$replacement .= '[u]';
249
							}
250
							elseif ($style_value == 'line-through')
251
							{
252
								$curCloseTags .= '[/s]';
253
								$replacement .= '[s]';
254
							}
255
						break;
256
257
						case 'text-align':
258
							if ($style_value == 'left')
259
							{
260
								$curCloseTags .= '[/left]';
261
								$replacement .= '[left]';
262
							}
263
							elseif ($style_value == 'center')
264
							{
265
								$curCloseTags .= '[/center]';
266
								$replacement .= '[center]';
267
							}
268
							elseif ($style_value == 'right')
269
							{
270
								$curCloseTags .= '[/right]';
271
								$replacement .= '[right]';
272
							}
273
						break;
274
275
						case 'font-style':
276
							if ($style_value == 'italic')
277
							{
278
								$curCloseTags .= '[/i]';
279
								$replacement .= '[i]';
280
							}
281
						break;
282
283
						case 'color':
284
							$curCloseTags .= '[/color]';
285
							$replacement .= '[color=' . $style_value . ']';
286
						break;
287
288
						case 'font-size':
289
							// Sometimes people put decimals where decimals should not be.
290
							if (preg_match('~(\d)+\.\d+(p[xt])~i', $style_value, $dec_matches) === 1)
291
								$style_value = $dec_matches[1] . $dec_matches[2];
292
293
							$curCloseTags .= '[/size]';
294
							$replacement .= '[size=' . $style_value . ']';
295
						break;
296
297
						case 'font-family':
298
							// Only get the first freaking font if there's a list!
299
							if (strpos($style_value, ',') !== false)
300
								$style_value = substr($style_value, 0, strpos($style_value, ','));
301
302
							$curCloseTags .= '[/font]';
303
							$replacement .= '[font=' . strtr($style_value, array("'" => '')) . ']';
304
						break;
305
306
						// This is a hack for images with dimensions embedded.
307
						case 'width':
308
						case 'height':
309
							if (preg_match('~[1-9]\d*~i', $style_value, $dimension) === 1)
310
								$extra_attr .= ' ' . $style_type . '="' . $dimension[0] . '"';
311
						break;
312
313
						case 'list-style-type':
314
							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)
315
								$extra_attr .= ' listtype="' . $listType[0] . '"';
316
						break;
317
					}
318
				}
319
320
				// Preserve some tags stripping the styling.
321
				if (in_array($matches[2], array('a', 'font', 'td')))
322
				{
323
					$replacement .= $precedingStyle . $afterStyle;
324
					$curCloseTags = '</' . $matches[2] . '>' . $curCloseTags;
325
				}
326
327
				// If there's something that still needs closing, push it to the stack.
328
				if (!empty($curCloseTags))
329
					array_push($stack, array(
330
							'element' => strtolower($curElement),
331
							'closeTags' => $curCloseTags
332
						)
333
					);
334
				elseif (!empty($extra_attr))
335
					$replacement .= $precedingStyle . $extra_attr . $afterStyle;
336
			}
337
		}
338
339
		elseif (preg_match('~</([A-Za-z]+)>~', $part, $matches) === 1)
340
		{
341
			// Is this the element that we've been waiting for to be closed?
342
			if (!empty($stack) && strtolower($matches[1]) === $stack[count($stack) - 1]['element'])
343
			{
344
				$byebyeTag = array_pop($stack);
345
				$replacement .= $byebyeTag['closeTags'];
346
			}
347
348
			// Must've been something else.
349
			else
350
				$replacement .= $part;
351
		}
352
		// In all other cases, just add the part to the replacement.
353
		else
354
			$replacement .= $part;
355
	}
356
357
	// Now put back the replacement in the text.
358
	$text = $replacement;
359
360
	// We are not finished yet, request more time.
361
	if (connection_aborted() && $context['server']['is_apache'])
362
		@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...
363
364
	// Let's pull out any legacy alignments.
365
	while (preg_match('~<([A-Za-z]+)\s+[^<>]*?(align="*(left|center|right)"*)[^<>]*?(/?)>~i', $text, $matches) === 1)
366
	{
367
		// Find the position in the text of this tag over again.
368
		$start_pos = strpos($text, $matches[0]);
369
		if ($start_pos === false)
370
			break;
371
372
		// End tag?
373
		if ($matches[4] != '/' && strpos($text, '</' . $matches[1] . '>', $start_pos) !== false)
374
		{
375
			$end_pos = strpos($text, '</' . $matches[1] . '>', $start_pos);
376
377
			// Remove the align from that tag so it's never checked again.
378
			$tag = substr($text, $start_pos, strlen($matches[0]));
379
			$content = substr($text, $start_pos + strlen($matches[0]), $end_pos - $start_pos - strlen($matches[0]));
380
			$tag = str_replace($matches[2], '', $tag);
381
382
			// Put the tags back into the body.
383
			$text = substr($text, 0, $start_pos) . $tag . '[' . $matches[3] . ']' . $content . '[/' . $matches[3] . ']' . substr($text, $end_pos);
384
		}
385
		else
386
		{
387
			// Just get rid of this evil tag.
388
			$text = substr($text, 0, $start_pos) . substr($text, $start_pos + strlen($matches[0]));
389
		}
390
	}
391
392
	// Let's do some special stuff for fonts - cause we all love fonts.
393
	while (preg_match('~<font\s+([^<>]*)>~i', $text, $matches) === 1)
394
	{
395
		// Find the position of this again.
396
		$start_pos = strpos($text, $matches[0]);
397
		$end_pos = false;
398
		if ($start_pos === false)
399
			break;
400
401
		// This must have an end tag - and we must find the right one.
402
		$lower_text = strtolower($text);
403
404
		$start_pos_test = $start_pos + 4;
405
		// How many starting tags must we find closing ones for first?
406
		$start_font_tag_stack = 0;
407
		while ($start_pos_test < strlen($text))
408
		{
409
			// Where is the next starting font?
410
			$next_start_pos = strpos($lower_text, '<font', $start_pos_test);
411
			$next_end_pos = strpos($lower_text, '</font>', $start_pos_test);
412
413
			// Did we past another starting tag before an end one?
414
			if ($next_start_pos !== false && $next_start_pos < $next_end_pos)
415
			{
416
				$start_font_tag_stack++;
417
				$start_pos_test = $next_start_pos + 4;
418
			}
419
			// Otherwise we have an end tag but not the right one?
420
			elseif ($start_font_tag_stack)
421
			{
422
				$start_font_tag_stack--;
423
				$start_pos_test = $next_end_pos + 4;
424
			}
425
			// Otherwise we're there!
426
			else
427
			{
428
				$end_pos = $next_end_pos;
429
				break;
430
			}
431
		}
432
		if ($end_pos === false)
433
			break;
434
435
		// Now work out what the attributes are.
436
		$attribs = fetchTagAttributes($matches[1]);
437
		$tags = array();
438
		$sizes_equivalence = array(1 => '8pt', '10pt', '12pt', '14pt', '18pt', '24pt', '36pt');
439
		foreach ($attribs as $s => $v)
440
		{
441
			if ($s == 'size')
442
			{
443
				// Cast before empty chech because casting a string results in a 0 and we don't have zeros in the array! ;)
444
				$v = (int) trim($v);
445
				$v = empty($v) ? 1 : $v;
446
				$tags[] = array('[size=' . $sizes_equivalence[$v] . ']', '[/size]');
447
			}
448 View Code Duplication
			elseif ($s == 'face')
449
				$tags[] = array('[font=' . trim(strtolower($v)) . ']', '[/font]');
450 View Code Duplication
			elseif ($s == 'color')
451
				$tags[] = array('[color=' . trim(strtolower($v)) . ']', '[/color]');
452
		}
453
454
		// As before add in our tags.
455
		$before = $after = '';
456
		foreach ($tags as $tag)
457
		{
458
			$before .= $tag[0];
459
			if (isset($tag[1]))
460
				$after = $tag[1] . $after;
461
		}
462
463
		// Remove the tag so it's never checked again.
464
		$content = substr($text, $start_pos + strlen($matches[0]), $end_pos - $start_pos - strlen($matches[0]));
465
466
		// Put the tags back into the body.
467
		$text = substr($text, 0, $start_pos) . $before . $content . $after . substr($text, $end_pos + 7);
468
	}
469
470
	// Almost there, just a little more time.
471
	if (connection_aborted() && $context['server']['is_apache'])
472
		@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...
473
474
	if (count($parts = preg_split('~<(/?)(li|ol|ul)([^>]*)>~i', $text, null, PREG_SPLIT_DELIM_CAPTURE)) > 1)
475
	{
476
		// A toggle that dermines whether we're directly under a <ol> or <ul>.
477
		$inList = false;
478
479
		// Keep track of the number of nested list levels.
480
		$listDepth = 0;
481
482
		// Map what we can expect from the HTML to what is supported by SMF.
483
		$listTypeMapping = array(
484
			'1' => 'decimal',
485
			'A' => 'upper-alpha',
486
			'a' => 'lower-alpha',
487
			'I' => 'upper-roman',
488
			'i' => 'lower-roman',
489
			'disc' => 'disc',
490
			'square' => 'square',
491
			'circle' => 'circle',
492
		);
493
494
		// $i: text, $i + 1: '/', $i + 2: tag, $i + 3: tail.
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
495
		for ($i = 0, $numParts = count($parts) - 1; $i < $numParts; $i += 4)
496
		{
497
			$tag = strtolower($parts[$i + 2]);
498
			$isOpeningTag = $parts[$i + 1] === '';
499
500
			if ($isOpeningTag)
501
			{
502
				switch ($tag)
503
				{
504
					case 'ol':
505
					case 'ul':
506
507
						// We have a problem, we're already in a list.
508
						if ($inList)
509
						{
510
							// Inject a list opener, we'll deal with the ol/ul next loop.
511
							array_splice($parts, $i, 0, array(
512
								'',
513
								'',
514
								str_repeat("\t", $listDepth) . '[li]',
515
								'',
516
							));
517
							$numParts = count($parts) - 1;
518
519
							// The inlist status changes a bit.
520
							$inList = false;
521
						}
522
523
						// Just starting a new list.
524
						else
525
						{
526
							$inList = true;
527
528
							if ($tag === 'ol')
529
								$listType = 'decimal';
530
							elseif (preg_match('~type="?(' . implode('|', array_keys($listTypeMapping)) . ')"?~', $parts[$i + 3], $match) === 1)
531
								$listType = $listTypeMapping[$match[1]];
532
							else
533
								$listType = null;
534
535
							$listDepth++;
536
537
							$parts[$i + 2] = '[list' . ($listType === null ? '' : ' type=' . $listType) . ']' . "\n";
538
							$parts[$i + 3] = '';
539
						}
540
					break;
541
542
					case 'li':
543
544
						// This is how it should be: a list item inside the list.
545
						if ($inList)
546
						{
547
							$parts[$i + 2] = str_repeat("\t", $listDepth) . '[li]';
548
							$parts[$i + 3] = '';
549
550
							// Within a list item, it's almost as if you're outside.
551
							$inList = false;
552
						}
553
554
						// The li is no direct child of a list.
555
						else
556
						{
557
							// We are apparently in a list item.
558
							if ($listDepth > 0)
559
							{
560
								$parts[$i + 2] = '[/li]' . "\n" . str_repeat("\t", $listDepth) . '[li]';
561
								$parts[$i + 3] = '';
562
							}
563
564
							// We're not even near a list.
565
							else
566
							{
567
								// Quickly create a list with an item.
568
								$listDepth++;
569
570
								$parts[$i + 2] = '[list]' . "\n\t" . '[li]';
571
								$parts[$i + 3] = '';
572
							}
573
						}
574
575
					break;
576
				}
577
			}
578
579
			// Handle all the closing tags.
580
			else
581
			{
582
				switch ($tag)
583
				{
584
					case 'ol':
585
					case 'ul':
586
587
						// As we expected it, closing the list while we're in it.
588
						if ($inList)
589
						{
590
							$inList = false;
591
592
							$listDepth--;
593
594
							$parts[$i + 1] = '';
595
							$parts[$i + 2] = str_repeat("\t", $listDepth) . '[/list]';
596
							$parts[$i + 3] = '';
597
						}
598
599
						else
600
						{
601
							// We're in a list item.
602
							if ($listDepth > 0)
603
							{
604
								// Inject closure for this list item first.
605
								// The content of $parts[$i] is left as is!
606
								array_splice($parts, $i + 1, 0, array(
607
									'', // $i + 1
608
									'[/li]' . "\n", // $i + 2
609
									'', // $i + 3
610
									'', // $i + 4
611
								));
612
								$numParts = count($parts) - 1;
613
614
								// Now that we've closed the li, we're in list space.
615
								$inList = true;
616
							}
617
618
							// We're not even in a list, ignore
619 View Code Duplication
							else
620
							{
621
								$parts[$i + 1] = '';
622
								$parts[$i + 2] = '';
623
								$parts[$i + 3] = '';
624
							}
625
						}
626
					break;
627
628
					case 'li':
629
630
						if ($inList)
631
						{
632
							// There's no use for a </li> after <ol> or <ul>, ignore.
633
							$parts[$i + 1] = '';
634
							$parts[$i + 2] = '';
635
							$parts[$i + 3] = '';
636
						}
637
638
						else
639
						{
640
							// Remove the trailing breaks from the list item.
641
							$parts[$i] = preg_replace('~\s*<br\s*' . '/?' . '>\s*$~', '', $parts[$i]);
642
							$parts[$i + 1] = '';
643
							$parts[$i + 2] = '[/li]' . "\n";
644
							$parts[$i + 3] = '';
645
646
							// And we're back in the [list] space.
647
							$inList = true;
648
						}
649
650
					break;
651
				}
652
			}
653
654
			// If we're in the [list] space, no content is allowed.
655
			if ($inList && trim(preg_replace('~\s*<br\s*' . '/?' . '>\s*~', '', $parts[$i + 4])) !== '')
656
			{
657
				// Fix it by injecting an extra list item.
658
				array_splice($parts, $i + 4, 0, array(
659
					'', // No content.
660
					'', // Opening tag.
661
					'li', // It's a <li>.
662
					'', // No tail.
663
				));
664
				$numParts = count($parts) - 1;
665
			}
666
		}
667
668
		$text = implode('', $parts);
669
670
		if ($inList)
671
		{
672
			$listDepth--;
673
			$text .= str_repeat("\t", $listDepth) . '[/list]';
674
		}
675
676
		for ($i = $listDepth; $i > 0; $i--)
677
			$text .= '[/li]' . "\n" . str_repeat("\t", $i - 1) . '[/list]';
678
679
	}
680
681
	// I love my own image...
682
	while (preg_match('~<img\s+([^<>]*)/*>~i', $text, $matches) === 1)
683
	{
684
		// Find the position of the image.
685
		$start_pos = strpos($text, $matches[0]);
686
		if ($start_pos === false)
687
			break;
688
		$end_pos = $start_pos + strlen($matches[0]);
689
690
		$params = '';
691
		$src = '';
692
693
		$attrs = fetchTagAttributes($matches[1]);
694
		foreach ($attrs as $attrib => $value)
695
		{
696
			if (in_array($attrib, array('width', 'height')))
697
				$params .= ' ' . $attrib . '=' . (int) $value;
698
			elseif ($attrib == 'alt' && trim($value) != '')
699
				$params .= ' alt=' . trim($value);
700
			elseif ($attrib == 'src')
701
				$src = trim($value);
702
		}
703
704
		$tag = '';
705
		if (!empty($src))
706
		{
707
			// Attempt to fix the path in case it's not present.
708 View Code Duplication
			if (preg_match('~^https?://~i', $src) === 0 && is_array($parsedURL = parse_url($scripturl)) && isset($parsedURL['host']))
709
			{
710
				$baseURL = (isset($parsedURL['scheme']) ? $parsedURL['scheme'] : 'http') . '://' . $parsedURL['host'] . (empty($parsedURL['port']) ? '' : ':' . $parsedURL['port']);
711
712
				if (substr($src, 0, 1) === '/')
713
					$src = $baseURL . $src;
714
				else
715
					$src = $baseURL . (empty($parsedURL['path']) ? '/' : preg_replace('~/(?:index\\.php)?$~', '', $parsedURL['path'])) . '/' . $src;
716
			}
717
718
			$tag = '[img' . $params . ']' . $src . '[/img]';
719
		}
720
721
		// Replace the tag
722
		$text = substr($text, 0, $start_pos) . $tag . substr($text, $end_pos);
723
	}
724
725
	// The final bits are the easy ones - tags which map to tags which map to tags - etc etc.
726
	$tags = array(
727
		'~<b(\s(.)*?)*?' . '>~i' => function()
728
		{
729
			return '[b]';
730
		},
731
		'~</b>~i' => function()
732
		{
733
			return '[/b]';
734
		},
735
		'~<i(\s(.)*?)*?' . '>~i' => function()
736
		{
737
			return '[i]';
738
		},
739
		'~</i>~i' => function()
740
		{
741
			return '[/i]';
742
		},
743
		'~<u(\s(.)*?)*?' . '>~i' => function()
744
		{
745
			return '[u]';
746
		},
747
		'~</u>~i' => function()
748
		{
749
			return '[/u]';
750
		},
751
		'~<strong(\s(.)*?)*?' . '>~i' => function()
752
		{
753
			return '[b]';
754
		},
755
		'~</strong>~i' => function()
756
		{
757
			return '[/b]';
758
		},
759
		'~<em(\s(.)*?)*?' . '>~i' => function()
760
		{
761
			return '[i]';
762
		},
763
		'~</em>~i' => function()
764
		{
765
			return '[i]';
766
		},
767
		'~<s(\s(.)*?)*?' . '>~i' => function()
768
		{
769
			return "[s]";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal [s] does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
770
		},
771
		'~</s>~i' => function()
772
		{
773
			return "[/s]";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal [/s] does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
774
		},
775
		'~<strike(\s(.)*?)*?' . '>~i' => function()
776
		{
777
			return '[s]';
778
		},
779
		'~</strike>~i' => function()
780
		{
781
			return '[/s]';
782
		},
783
		'~<del(\s(.)*?)*?' . '>~i' => function()
784
		{
785
			return '[s]';
786
		},
787
		'~</del>~i' => function()
788
		{
789
			return '[/s]';
790
		},
791
		'~<center(\s(.)*?)*?' . '>~i' => function()
792
		{
793
			return '[center]';
794
		},
795
		'~</center>~i' => function()
796
		{
797
			return '[/center]';
798
		},
799
		'~<pre(\s(.)*?)*?' . '>~i' => function()
800
		{
801
			return '[pre]';
802
		},
803
		'~</pre>~i' => function()
804
		{
805
			return '[/pre]';
806
		},
807
		'~<sub(\s(.)*?)*?' . '>~i' => function()
808
		{
809
			return '[sub]';
810
		},
811
		'~</sub>~i' => function()
812
		{
813
			return '[/sub]';
814
		},
815
		'~<sup(\s(.)*?)*?' . '>~i' => function()
816
		{
817
			return '[sup]';
818
		},
819
		'~</sup>~i' => function()
820
		{
821
			return '[/sup]';
822
		},
823
		'~<tt(\s(.)*?)*?' . '>~i' => function()
824
		{
825
			return '[tt]';
826
		},
827
		'~</tt>~i' => function()
828
		{
829
			return '[/tt]';
830
		},
831
		'~<table(\s(.)*?)*?' . '>~i' => function()
832
		{
833
			return '[table]';
834
		},
835
		'~</table>~i' => function()
836
		{
837
			return '[/table]';
838
		},
839
		'~<tr(\s(.)*?)*?' . '>~i' => function()
840
		{
841
			return '[tr]';
842
		},
843
		'~</tr>~i' => function()
844
		{
845
			return '[/tr]';
846
		},
847
		'~<(td|th)\s[^<>]*?colspan="?(\d{1,2})"?.*?' . '>~i' => function($matches)
848
		{
849
			return str_repeat('[td][/td]', $matches[2] - 1) . '[td]';
850
		},
851
		'~<(td|th)(\s(.)*?)*?' . '>~i' => function()
852
		{
853
			return '[td]';
854
		},
855
		'~</(td|th)>~i' => function()
856
		{
857
			return '[/td]';
858
		},
859
		'~<br(?:\s[^<>]*?)?' . '>~i' => function()
860
		{
861
			return "\n";
862
		},
863
		'~<hr[^<>]*>(\n)?~i' => function($matches)
864
		{
865
			return "[hr]\n" . $matches[0];
866
		},
867
		'~(\n)?\\[hr\\]~i' => function()
868
		{
869
			return "\n[hr]";
870
		},
871
		'~^\n\\[hr\\]~i' => function()
872
		{
873
			return "[hr]";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal [hr] does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
874
		},
875
		'~<blockquote(\s(.)*?)*?' . '>~i' =>  function()
876
		{
877
			return "&lt;blockquote&gt;";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal <blockquote> does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
878
		},
879
		'~</blockquote>~i' => function()
880
		{
881
			return "&lt;/blockquote&gt;";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal </blockquote> does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
882
		},
883
		'~<ins(\s(.)*?)*?' . '>~i' => function()
884
		{
885
			return "&lt;ins&gt;";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal <ins> does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
886
		},
887
		'~</ins>~i' => function()
888
		{
889
			return "&lt;/ins&gt;";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal </ins> does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
890
		},
891
	);
892
893
	foreach ($tags as $tag => $replace)
894
		$text = preg_replace_callback($tag, $replace, $text);
895
896
	// Please give us just a little more time.
897
	if (connection_aborted() && $context['server']['is_apache'])
898
		@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...
899
900
	// What about URL's - the pain in the ass of the tag world.
901
	while (preg_match('~<a\s+([^<>]*)>([^<>]*)</a>~i', $text, $matches) === 1)
902
	{
903
		// Find the position of the URL.
904
		$start_pos = strpos($text, $matches[0]);
905
		if ($start_pos === false)
906
			break;
907
		$end_pos = $start_pos + strlen($matches[0]);
908
909
		$tag_type = 'url';
910
		$href = '';
911
912
		$attrs = fetchTagAttributes($matches[1]);
913
		foreach ($attrs as $attrib => $value)
914
		{
915
			if ($attrib == 'href')
916
			{
917
				$href = trim($value);
918
919
				// Are we dealing with an FTP link?
920
				if (preg_match('~^ftps?://~', $href) === 1)
921
					$tag_type = 'ftp';
922
923
				// Or is this a link to an email address?
924
				elseif (substr($href, 0, 7) == 'mailto:')
925
				{
926
					$tag_type = 'email';
927
					$href = substr($href, 7);
928
				}
929
930
				// No http(s), so attempt to fix this potential relative URL.
931 View Code Duplication
				elseif (preg_match('~^https?://~i', $href) === 0 && is_array($parsedURL = parse_url($scripturl)) && isset($parsedURL['host']))
932
				{
933
					$baseURL = (isset($parsedURL['scheme']) ? $parsedURL['scheme'] : 'http') . '://' . $parsedURL['host'] . (empty($parsedURL['port']) ? '' : ':' . $parsedURL['port']);
934
935
					if (substr($href, 0, 1) === '/')
936
						$href = $baseURL . $href;
937
					else
938
						$href = $baseURL . (empty($parsedURL['path']) ? '/' : preg_replace('~/(?:index\\.php)?$~', '', $parsedURL['path'])) . '/' . $href;
939
				}
940
			}
941
942
			// External URL?
943
			if ($attrib == 'target' && $tag_type == 'url')
944
			{
945
				if (trim($value) == '_blank')
946
					$tag_type == 'iurl';
947
			}
948
		}
949
950
		$tag = '';
951
		if ($href != '')
952
		{
953
			if ($matches[2] == $href)
954
				$tag = '[' . $tag_type . ']' . $href . '[/' . $tag_type . ']';
955
			else
956
				$tag = '[' . $tag_type . '=' . $href . ']' . $matches[2] . '[/' . $tag_type . ']';
957
		}
958
959
		// Replace the tag
960
		$text = substr($text, 0, $start_pos) . $tag . substr($text, $end_pos);
961
	}
962
963
	$text = strip_tags($text);
964
965
	// Some tags often end up as just dummy tags - remove those.
966
	$text = preg_replace('~\[[bisu]\]\s*\[/[bisu]\]~', '', $text);
967
968
	// Fix up entities.
969
	$text = preg_replace('~&#38;~i', '&#38;#38;', $text);
970
971
	$text = legalise_bbc($text);
972
973
	return $text;
974
}
975
976
/**
977
 * !!!Compatibility!!!
978
 * This is no more needed, but to avoid break mods let's keep it
979
 *
980
 * Returns an array of attributes associated with a tag.
981
 *
982
 * @param string $text A tag
983
 * @return array An array of attributes
984
 */
985
function fetchTagAttributes($text)
986
{
987
	$attribs = array();
988
	$key = $value = '';
989
	$tag_state = 0; // 0 = key, 1 = attribute with no string, 2 = attribute with string
990
	for ($i = 0; $i < strlen($text); $i++)
991
	{
992
		// We're either moving from the key to the attribute or we're in a string and this is fine.
993
		if ($text[$i] == '=')
994
		{
995
			if ($tag_state == 0)
996
				$tag_state = 1;
997
			elseif ($tag_state == 2)
998
				$value .= '=';
999
		}
1000
		// A space is either moving from an attribute back to a potential key or in a string is fine.
1001
		elseif ($text[$i] == ' ')
1002
		{
1003
			if ($tag_state == 2)
1004
				$value .= ' ';
1005
			elseif ($tag_state == 1)
1006
			{
1007
				$attribs[$key] = $value;
1008
				$key = $value = '';
1009
				$tag_state = 0;
1010
			}
1011
		}
1012
		// A quote?
1013
		elseif ($text[$i] == '"')
1014
		{
1015
			// Must be either going into or out of a string.
1016
			if ($tag_state == 1)
1017
				$tag_state = 2;
1018
			else
1019
				$tag_state = 1;
1020
		}
1021
		// Otherwise it's fine.
1022
		else
1023
		{
1024
			if ($tag_state == 0)
1025
				$key .= $text[$i];
1026
			else
1027
				$value .= $text[$i];
1028
		}
1029
	}
1030
1031
	// Anything left?
1032
	if ($key != '' && $value != '')
1033
		$attribs[$key] = $value;
1034
1035
	return $attribs;
1036
}
1037
1038
/**
1039
 * !!!Compatibility!!!
1040
 * Attempt to clean up illegal BBC caused by browsers like Opera which don't obey the rules
1041
 * @param string $text Text
1042
 * @return string Cleaned up text
1043
 */
1044
function legalise_bbc($text)
1045
{
1046
	global $modSettings;
1047
1048
	// Don't care about the texts that are too short.
1049
	if (strlen($text) < 3)
1050
		return $text;
1051
1052
	// A list of tags that's disabled by the admin.
1053
	$disabled = empty($modSettings['disabledBBC']) ? array() : array_flip(explode(',', strtolower($modSettings['disabledBBC'])));
1054
1055
	// Add flash if it's disabled as embedded tag.
1056
	if (empty($modSettings['enableEmbeddedFlash']))
1057
		$disabled['flash'] = true;
1058
1059
	// Get a list of all the tags that are not disabled.
1060
	$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...
1061
	$valid_tags = array();
1062
	$self_closing_tags = array();
1063
	foreach ($all_tags as $tag)
0 ignored issues
show
Bug introduced by
The expression $all_tags of type string is not traversable.
Loading history...
1064
	{
1065
		if (!isset($disabled[$tag['tag']]))
1066
			$valid_tags[$tag['tag']] = !empty($tag['block_level']);
1067
		if (isset($tag['type']) && $tag['type'] == 'closed')
1068
			$self_closing_tags[] = $tag['tag'];
1069
	}
1070
1071
	// 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!
1072
	$align_tags = array('left', 'center', 'right', 'pre');
1073
1074
	// Remove those align tags that are not valid.
1075
	$align_tags = array_intersect($align_tags, array_keys($valid_tags));
1076
1077
	// These keep track of where we are!
1078
	if (!empty($align_tags) && count($matches = preg_split('~(\\[/?(?:' . implode('|', $align_tags) . ')\\])~', $text, -1, PREG_SPLIT_DELIM_CAPTURE)) > 1)
1079
	{
1080
		// The first one is never a tag.
1081
		$isTag = false;
1082
1083
		// By default we're not inside a tag too.
1084
		$insideTag = null;
1085
1086
		foreach ($matches as $i => $match)
1087
		{
1088
			// We're only interested in tags, not text.
1089
			if ($isTag)
1090
			{
1091
				$isClosingTag = substr($match, 1, 1) === '/';
1092
				$tagName = substr($match, $isClosingTag ? 2 : 1, -1);
1093
1094
				// We're closing the exact same tag that we opened.
1095
				if ($isClosingTag && $insideTag === $tagName)
1096
					$insideTag = null;
1097
1098
				// We're opening a tag and we're not yet inside one either
1099
				elseif (!$isClosingTag && $insideTag === null)
1100
					$insideTag = $tagName;
1101
1102
				// In all other cases, this tag must be invalid
1103
				else
1104
					unset($matches[$i]);
1105
			}
1106
1107
			// The next one is gonna be the other one.
1108
			$isTag = !$isTag;
1109
		}
1110
1111
		// We're still inside a tag and had no chance for closure?
1112
		if ($insideTag !== null)
1113
			$matches[] = '[/' . $insideTag . ']';
1114
1115
		// And a complete text string again.
1116
		$text = implode('', $matches);
1117
	}
1118
1119
	// Quickly remove any tags which are back to back.
1120
	$backToBackPattern = '~\\[(' . implode('|', array_diff(array_keys($valid_tags), array('td', 'anchor'))) . ')[^<>\\[\\]]*\\]\s*\\[/\\1\\]~';
1121
	$lastlen = 0;
1122 View Code Duplication
	while (strlen($text) !== $lastlen)
1123
		$lastlen = strlen($text = preg_replace($backToBackPattern, '', $text));
1124
1125
	// Need to sort the tags my name length.
1126
	uksort($valid_tags, 'sort_array_length');
1127
1128
	// These inline tags can compete with each other regarding style.
1129
	$competing_tags = array(
1130
		'color',
1131
		'size',
1132
	);
1133
1134
	// These keep track of where we are!
1135
	if (count($parts = preg_split(sprintf('~(\\[)(/?)(%1$s)((?:[\\s=][^\\]\\[]*)?\\])~', implode('|', array_keys($valid_tags))), $text, -1, PREG_SPLIT_DELIM_CAPTURE)) > 1)
1136
	{
1137
		// Start outside [nobbc] or [code] blocks.
1138
		$inCode = false;
1139
		$inNoBbc = false;
1140
1141
		// A buffer containing all opened inline elements.
1142
		$inlineElements = array();
1143
1144
		// A buffer containing all opened block elements.
1145
		$blockElements = array();
1146
1147
		// A buffer containing the opened inline elements that might compete.
1148
		$competingElements = array();
1149
1150
		// $i: text, $i + 1: '[', $i + 2: '/', $i + 3: tag, $i + 4: tag tail.
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1151
		for ($i = 0, $n = count($parts) - 1; $i < $n; $i += 5)
1152
		{
1153
			$tag = $parts[$i + 3];
1154
			$isOpeningTag = $parts[$i + 2] === '';
1155
			$isClosingTag = $parts[$i + 2] === '/';
1156
			$isBlockLevelTag = isset($valid_tags[$tag]) && $valid_tags[$tag] && !in_array($tag, $self_closing_tags);
1157
			$isCompetingTag = in_array($tag, $competing_tags);
1158
1159
			// Check if this might be one of those cleaned out tags.
1160
			if ($tag === '')
1161
				continue;
1162
1163
			// Special case: inside [code] blocks any code is left untouched.
1164 View Code Duplication
			elseif ($tag === 'code')
1165
			{
1166
				// We're inside a code block and closing it.
1167
				if ($inCode && $isClosingTag)
1168
				{
1169
					$inCode = false;
1170
1171
					// Reopen tags that were closed before the code block.
1172
					if (!empty($inlineElements))
1173
						$parts[$i + 4] .= '[' . implode('][', array_keys($inlineElements)) . ']';
1174
				}
1175
1176
				// We're outside a coding and nobbc block and opening it.
1177
				elseif (!$inCode && !$inNoBbc && $isOpeningTag)
1178
				{
1179
					// If there are still inline elements left open, close them now.
1180
					if (!empty($inlineElements))
1181
					{
1182
						$parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']';
1183
						//$inlineElements = array();
0 ignored issues
show
Unused Code Comprehensibility introduced by
63% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1184
					}
1185
1186
					$inCode = true;
1187
				}
1188
1189
				// Nothing further to do.
1190
				continue;
1191
			}
1192
1193
			// Special case: inside [nobbc] blocks any BBC is left untouched.
1194 View Code Duplication
			elseif ($tag === 'nobbc')
1195
			{
1196
				// We're inside a nobbc block and closing it.
1197
				if ($inNoBbc && $isClosingTag)
1198
				{
1199
					$inNoBbc = false;
1200
1201
					// Some inline elements might've been closed that need reopening.
1202
					if (!empty($inlineElements))
1203
						$parts[$i + 4] .= '[' . implode('][', array_keys($inlineElements)) . ']';
1204
				}
1205
1206
				// We're outside a nobbc and coding block and opening it.
1207
				elseif (!$inNoBbc && !$inCode && $isOpeningTag)
1208
				{
1209
					// Can't have inline elements still opened.
1210
					if (!empty($inlineElements))
1211
					{
1212
						$parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']';
1213
						//$inlineElements = array();
0 ignored issues
show
Unused Code Comprehensibility introduced by
63% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1214
					}
1215
1216
					$inNoBbc = true;
1217
				}
1218
1219
				continue;
1220
			}
1221
1222
			// So, we're inside one of the special blocks: ignore any tag.
1223
			elseif ($inCode || $inNoBbc)
1224
				continue;
1225
1226
			// We're dealing with an opening tag.
1227
			if ($isOpeningTag)
1228
			{
1229
				// Everyting inside the square brackets of the opening tag.
1230
				$elementContent = $parts[$i + 3] . substr($parts[$i + 4], 0, -1);
1231
1232
				// A block level opening tag.
1233
				if ($isBlockLevelTag)
1234
				{
1235
					// Are there inline elements still open?
1236 View Code Duplication
					if (!empty($inlineElements))
1237
					{
1238
						// Close all the inline tags, a block tag is coming...
1239
						$parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']';
1240
1241
						// Now open them again, we're inside the block tag now.
1242
						$parts[$i + 5] = '[' . implode('][', array_keys($inlineElements)) . ']' . $parts[$i + 5];
1243
					}
1244
1245
					$blockElements[] = $tag;
1246
				}
1247
1248
				// Inline opening tag.
1249
				elseif (!in_array($tag, $self_closing_tags))
1250
				{
1251
					// Can't have two opening elements with the same contents!
1252
					if (isset($inlineElements[$elementContent]))
1253
					{
1254
						// Get rid of this tag.
1255
						$parts[$i + 1] = $parts[$i + 2] = $parts[$i + 3] = $parts[$i + 4] = '';
1256
1257
						// Now try to find the corresponding closing tag.
1258
						$curLevel = 1;
1259
						for ($j = $i + 5, $m = count($parts) - 1; $j < $m; $j += 5)
1260
						{
1261
							// Find the tags with the same tagname
1262
							if ($parts[$j + 3] === $tag)
1263
							{
1264
								// If it's an opening tag, increase the level.
1265
								if ($parts[$j + 2] === '')
1266
									$curLevel++;
1267
1268
								// A closing tag, decrease the level.
1269
								else
1270
								{
1271
									$curLevel--;
1272
1273
									// Gotcha! Clean out this closing tag gone rogue.
1274
									if ($curLevel === 0)
1275
									{
1276
										$parts[$j + 1] = $parts[$j + 2] = $parts[$j + 3] = $parts[$j + 4] = '';
1277
										break;
1278
									}
1279
								}
1280
							}
1281
						}
1282
					}
1283
1284
					// Otherwise, add this one to the list.
1285
					else
1286
					{
1287
						if ($isCompetingTag)
1288
						{
1289
							if (!isset($competingElements[$tag]))
1290
								$competingElements[$tag] = array();
1291
1292
							$competingElements[$tag][] = $parts[$i + 4];
1293
1294
							if (count($competingElements[$tag]) > 1)
1295
								$parts[$i] .= '[/' . $tag . ']';
1296
						}
1297
1298
						$inlineElements[$elementContent] = $tag;
1299
					}
1300
				}
1301
1302
			}
1303
1304
			// Closing tag.
1305
			else
1306
			{
1307
				// Closing the block tag.
1308
				if ($isBlockLevelTag)
1309
				{
1310
					// Close the elements that should've been closed by closing this tag.
1311
					if (!empty($blockElements))
1312
					{
1313
						$addClosingTags = array();
1314
						while ($element = array_pop($blockElements))
1315
						{
1316
							if ($element === $tag)
1317
								break;
1318
1319
							// Still a block tag was open not equal to this tag.
1320
							$addClosingTags[] = $element['type'];
1321
						}
1322
1323
						if (!empty($addClosingTags))
1324
							$parts[$i + 1] = '[/' . implode('][/', array_reverse($addClosingTags)) . ']' . $parts[$i + 1];
1325
1326
						// Apparently the closing tag was not found on the stack.
1327
						if (!is_string($element) || $element !== $tag)
1328
						{
1329
							// Get rid of this particular closing tag, it was never opened.
1330
							$parts[$i + 1] = substr($parts[$i + 1], 0, -1);
1331
							$parts[$i + 2] = $parts[$i + 3] = $parts[$i + 4] = '';
1332
							continue;
1333
						}
1334
					}
1335 View Code Duplication
					else
1336
					{
1337
						// Get rid of this closing tag!
1338
						$parts[$i + 1] = $parts[$i + 2] = $parts[$i + 3] = $parts[$i + 4] = '';
1339
						continue;
1340
					}
1341
1342
					// Inline elements are still left opened?
1343 View Code Duplication
					if (!empty($inlineElements))
1344
					{
1345
						// Close them first..
1346
						$parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']';
1347
1348
						// Then reopen them.
1349
						$parts[$i + 5] = '[' . implode('][', array_keys($inlineElements)) . ']' . $parts[$i + 5];
1350
					}
1351
				}
1352
				// Inline tag.
1353
				else
1354
				{
1355
					// Are we expecting this tag to end?
1356
					if (in_array($tag, $inlineElements))
1357
					{
1358
						foreach (array_reverse($inlineElements, true) as $tagContentToBeClosed => $tagToBeClosed)
1359
						{
1360
							// Closing it one way or the other.
1361
							unset($inlineElements[$tagContentToBeClosed]);
1362
1363
							// Was this the tag we were looking for?
1364
							if ($tagToBeClosed === $tag)
1365
								break;
1366
1367
							// Nope, close it and look further!
1368
							else
1369
								$parts[$i] .= '[/' . $tagToBeClosed . ']';
1370
						}
1371
1372
						if ($isCompetingTag && !empty($competingElements[$tag]))
1373
						{
1374
							array_pop($competingElements[$tag]);
1375
1376
							if (count($competingElements[$tag]) > 0)
1377
								$parts[$i + 5] = '[' . $tag . $competingElements[$tag][count($competingElements[$tag]) - 1] . $parts[$i + 5];
1378
						}
1379
					}
1380
1381
					// Unexpected closing tag, ex-ter-mi-nate.
1382 View Code Duplication
					else
1383
						$parts[$i + 1] = $parts[$i + 2] = $parts[$i + 3] = $parts[$i + 4] = '';
1384
				}
1385
			}
1386
		}
1387
1388
		// Close the code tags.
1389
		if ($inCode)
1390
			$parts[$i] .= '[/code]';
1391
1392
		// The same for nobbc tags.
1393
		elseif ($inNoBbc)
1394
			$parts[$i] .= '[/nobbc]';
1395
1396
		// Still inline tags left unclosed? Close them now, better late than never.
1397
		elseif (!empty($inlineElements))
1398
			$parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']';
1399
1400
		// Now close the block elements.
1401
		if (!empty($blockElements))
1402
			$parts[$i] .= '[/' . implode('][/', array_reverse($blockElements)) . ']';
1403
1404
		$text = implode('', $parts);
1405
	}
1406
1407
	// Final clean up of back to back tags.
1408
	$lastlen = 0;
1409 View Code Duplication
	while (strlen($text) !== $lastlen)
1410
		$lastlen = strlen($text = preg_replace($backToBackPattern, '', $text));
1411
1412
	return $text;
1413
}
1414
1415
/**
1416
 * !!!Compatibility!!!
1417
 * A help function for legalise_bbc for sorting arrays based on length.
1418
 * @param string $a A string
1419
 * @param string $b Another string
1420
 * @return int 1 if $a is shorter than $b, -1 otherwise
1421
 */
1422
function sort_array_length($a, $b)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $a. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
Comprehensibility introduced by
Avoid variables with short names like $b. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
1423
{
1424
	return strlen($a) < strlen($b) ? 1 : -1;
1425
}
1426
1427
/**
1428
 * Creates the javascript code for localization of the editor (SCEditor)
1429
 */
1430
function loadLocale()
1431
{
1432
	global $context, $txt, $editortxt, $modSettings;
1433
1434
	loadLanguage('Editor');
1435
1436
	$context['template_layers'] = array();
1437
	// Lets make sure we aren't going to output anything nasty.
1438
	@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...
1439
	if (!empty($modSettings['enableCompressedOutput']))
1440
		@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...
1441
	else
1442
		@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...
1443
1444
	// If we don't have any locale better avoid broken js
1445
	if (empty($txt['lang_locale']))
1446
		die();
1447
1448
	$file_data = '(function ($) {
1449
	\'use strict\';
1450
1451
	$.sceditor.locale[' . javaScriptEscape($txt['lang_locale']) . '] = {';
1452
	foreach ($editortxt as $key => $val)
1453
		$file_data .= '
1454
		' . javaScriptEscape($key) . ': ' . javaScriptEscape($val) . ',';
1455
1456
	$file_data .= '
1457
		dateFormat: "day.month.year"
1458
	}
1459
})(jQuery);';
1460
1461
	// Make sure they know what type of file we are.
1462
	header('Content-Type: text/javascript');
1463
	echo $file_data;
1464
	obExit(false);
1465
}
1466
1467
/**
1468
 * Retrieves a list of message icons.
1469
 * - Based on the settings, the array will either contain a list of default
1470
 *   message icons or a list of custom message icons retrieved from the database.
1471
 * - The board_id is needed for the custom message icons (which can be set for
1472
 *   each board individually).
1473
 *
1474
 * @param int $board_id The ID of the board
1475
 * @return array An array of info about available icons
1476
 */
1477
function getMessageIcons($board_id)
1478
{
1479
	global $modSettings, $txt, $settings, $smcFunc;
1480
1481
	if (empty($modSettings['messageIcons_enable']))
1482
	{
1483
		loadLanguage('Post');
1484
1485
		$icons = array(
1486
			array('value' => 'xx', 'name' => $txt['standard']),
1487
			array('value' => 'thumbup', 'name' => $txt['thumbs_up']),
1488
			array('value' => 'thumbdown', 'name' => $txt['thumbs_down']),
1489
			array('value' => 'exclamation', 'name' => $txt['exclamation_point']),
1490
			array('value' => 'question', 'name' => $txt['question_mark']),
1491
			array('value' => 'lamp', 'name' => $txt['lamp']),
1492
			array('value' => 'smiley', 'name' => $txt['icon_smiley']),
1493
			array('value' => 'angry', 'name' => $txt['icon_angry']),
1494
			array('value' => 'cheesy', 'name' => $txt['icon_cheesy']),
1495
			array('value' => 'grin', 'name' => $txt['icon_grin']),
1496
			array('value' => 'sad', 'name' => $txt['icon_sad']),
1497
			array('value' => 'wink', 'name' => $txt['icon_wink']),
1498
			array('value' => 'poll', 'name' => $txt['icon_poll']),
1499
		);
1500
1501
		foreach ($icons as $k => $dummy)
1502
		{
1503
			$icons[$k]['url'] = $settings['images_url'] . '/post/' . $dummy['value'] . '.png';
1504
			$icons[$k]['is_last'] = false;
1505
		}
1506
	}
1507
	// Otherwise load the icons, and check we give the right image too...
1508
	else
1509
	{
1510
		if (($temp = cache_get_data('posting_icons-' . $board_id, 480)) == null)
1511
		{
1512
			$request = $smcFunc['db_query']('', '
1513
				SELECT title, filename
1514
				FROM {db_prefix}message_icons
1515
				WHERE id_board IN (0, {int:board_id})
1516
				ORDER BY icon_order',
1517
				array(
1518
					'board_id' => $board_id,
1519
				)
1520
			);
1521
			$icon_data = array();
1522
			while ($row = $smcFunc['db_fetch_assoc']($request))
1523
				$icon_data[] = $row;
1524
			$smcFunc['db_free_result']($request);
1525
1526
			$icons = array();
1527
			foreach ($icon_data as $icon)
1528
			{
1529
				$icons[$icon['filename']] = array(
1530
					'value' => $icon['filename'],
1531
					'name' => $icon['title'],
1532
					'url' => $settings[file_exists($settings['theme_dir'] . '/images/post/' . $icon['filename'] . '.png') ? 'images_url' : 'default_images_url'] . '/post/' . $icon['filename'] . '.png',
1533
					'is_last' => false,
1534
				);
1535
			}
1536
1537
			cache_put_data('posting_icons-' . $board_id, $icons, 480);
1538
		}
1539
		else
1540
			$icons = $temp;
1541
	}
1542
	call_integration_hook('integrate_load_message_icons', array(&$icons));
1543
1544
	return array_values($icons);
1545
}
1546
1547
/**
1548
 * Compatibility function - used in 1.1 for showing a post box.
1549
 *
1550
 * @param string $msg The message
1551
 * @return string The HTML for an editor
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1552
 */
1553
function theme_postbox($msg)
0 ignored issues
show
Unused Code introduced by
The parameter $msg is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1554
{
1555
	global $context;
1556
1557
	return template_control_richedit($context['post_box_name']);
1558
}
1559
1560
/**
1561
 * Creates a box that can be used for richedit stuff like BBC, Smileys etc.
1562
 * @param array $editorOptions Various options for the editor
1563
 */
1564
function create_control_richedit($editorOptions)
1565
{
1566
	global $txt, $modSettings, $options, $smcFunc, $editortxt;
1567
	global $context, $settings, $user_info, $scripturl;
1568
1569
	// Load the Post language file... for the moment at least.
1570
	loadLanguage('Post');
1571
1572
	// Every control must have a ID!
1573
	assert(isset($editorOptions['id']));
1574
	assert(isset($editorOptions['value']));
1575
1576
	// Is this the first richedit - if so we need to ensure some template stuff is initialised.
1577
	if (empty($context['controls']['richedit']))
1578
	{
1579
		// Some general stuff.
1580
		$settings['smileys_url'] = $modSettings['smileys_url'] . '/' . $user_info['smiley_set'];
1581
		if (!empty($context['drafts_autosave']))
1582
			$context['drafts_autosave_frequency'] = empty($modSettings['drafts_autosave_frequency']) ? 60000 : $modSettings['drafts_autosave_frequency'] * 1000;
1583
1584
		// This really has some WYSIWYG stuff.
1585
		loadCSSFile('jquery.sceditor.css', array('force_current' => false, 'validate' => true), 'smf_jquery_sceditor');
1586
		loadTemplate('GenericControls');
1587
1588
		// JS makes the editor go round
1589
		loadJavaScriptFile('editor.js', array(), 'smf_editor');
1590
		loadJavaScriptFile('jquery.sceditor.bbcode.min.js', array(), 'smf_sceditor_bbcode');
1591
		loadJavaScriptFile('jquery.sceditor.smf.js', array(), 'smf_sceditor_smf');
1592
		addInlineJavaScript('
1593
		var smf_smileys_url = \'' . $settings['smileys_url'] . '\';
1594
		var bbc_quote_from = \'' . addcslashes($txt['quote_from'], "'") . '\';
1595
		var bbc_quote = \'' . addcslashes($txt['quote'], "'") . '\';
1596
		var bbc_search_on = \'' . addcslashes($txt['search_on'], "'") . '\';');
1597
		// editor language file
1598
		if (!empty($txt['lang_locale']) && $txt['lang_locale'] != 'en_US')
1599
			loadJavaScriptFile($scripturl . '?action=loadeditorlocale', array('external' => true), 'sceditor_language');
1600
1601
		$context['shortcuts_text'] = $txt['shortcuts' . (!empty($context['drafts_save']) ? '_drafts' : '') . (stripos($_SERVER['HTTP_USER_AGENT'], 'Macintosh') !== false ? '_mac' : (isBrowser('is_firefox') ? '_firefox' : ''))];
1602
		$context['show_spellchecking'] = !empty($modSettings['enableSpellChecking']) && (function_exists('pspell_new') || (function_exists('enchant_broker_init') && ($txt['lang_charset'] == 'UTF-8' || function_exists('iconv'))));
1603
		if ($context['show_spellchecking'])
1604
		{
1605
			loadJavaScriptFile('spellcheck.js', array(), 'smf_spellcheck');
1606
1607
			// Some hidden information is needed in order to make the spell checking work.
1608
			if (!isset($_REQUEST['xml']))
1609
				$context['insert_after_template'] .= '
1610
		<form name="spell_form" id="spell_form" method="post" accept-charset="' . $context['character_set'] . '" target="spellWindow" action="' . $scripturl . '?action=spellcheck">
1611
			<input type="hidden" name="spellstring" value="">
1612
		</form>';
1613
		}
1614
	}
1615
1616
	// Start off the editor...
1617
	$context['controls']['richedit'][$editorOptions['id']] = array(
1618
		'id' => $editorOptions['id'],
1619
		'value' => $editorOptions['value'],
1620
		'rich_value' => $editorOptions['value'], // 2.0 editor compatibility
1621
		'rich_active' => empty($modSettings['disable_wysiwyg']) && (!empty($options['wysiwyg_default']) || !empty($editorOptions['force_rich']) || !empty($_REQUEST[$editorOptions['id'] . '_mode'])),
1622
		'disable_smiley_box' => !empty($editorOptions['disable_smiley_box']),
1623
		'columns' => isset($editorOptions['columns']) ? $editorOptions['columns'] : 60,
1624
		'rows' => isset($editorOptions['rows']) ? $editorOptions['rows'] : 18,
1625
		'width' => isset($editorOptions['width']) ? $editorOptions['width'] : '70%',
1626
		'height' => isset($editorOptions['height']) ? $editorOptions['height'] : '250px',
1627
		'form' => isset($editorOptions['form']) ? $editorOptions['form'] : 'postmodify',
1628
		'bbc_level' => !empty($editorOptions['bbc_level']) ? $editorOptions['bbc_level'] : 'full',
1629
		'preview_type' => isset($editorOptions['preview_type']) ? (int) $editorOptions['preview_type'] : 1,
1630
		'labels' => !empty($editorOptions['labels']) ? $editorOptions['labels'] : array(),
1631
		'locale' => !empty($txt['lang_locale']) && substr($txt['lang_locale'], 0, 5) != 'en_US' ? $txt['lang_locale'] : '',
1632
		'required' => !empty($editorOptions['required']),
1633
	);
1634
1635
	if (empty($context['bbc_tags']))
1636
	{
1637
		// 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!
1638
		/*
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1639
			array(
1640
				'image' => 'bold',
1641
				'code' => 'b',
1642
				'before' => '[b]',
1643
				'after' => '[/b]',
1644
				'description' => $txt['bold'],
1645
			),
1646
		*/
1647
		$context['bbc_tags'] = array();
1648
		$context['bbc_tags'][] = array(
1649
			array(
1650
				'code' => 'bold',
1651
				'description' => $editortxt['bold'],
1652
			),
1653
			array(
1654
				'code' => 'italic',
1655
				'description' => $editortxt['italic'],
1656
			),
1657
			array(
1658
				'code' => 'underline',
1659
				'description' => $editortxt['underline']
1660
			),
1661
			array(
1662
				'code' => 'strike',
1663
				'description' => $editortxt['strike']
1664
			),
1665
			array(),
1666
			array(
1667
				'code' => 'pre',
1668
				'description' => $editortxt['preformatted']
1669
			),
1670
			array(
1671
				'code' => 'left',
1672
				'description' => $editortxt['left_align']
1673
			),
1674
			array(
1675
				'code' => 'center',
1676
				'description' => $editortxt['center']
1677
			),
1678
			array(
1679
				'code' => 'right',
1680
				'description' => $editortxt['right_align']
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' => 'image',
1699
				'description' => $editortxt['image']
1700
			),
1701
			array(
1702
				'code' => 'link',
1703
				'description' => $editortxt['hyperlink']
1704
			),
1705
			array(
1706
				'code' => 'email',
1707
				'description' => $editortxt['insert_email']
1708
			),
1709
			array(),
1710
			array(
1711
				'code' => 'superscript',
1712
				'description' => $editortxt['superscript']
1713
			),
1714
			array(
1715
				'code' => 'subscript',
1716
				'description' => $editortxt['subscript']
1717
			),
1718
			array(),
1719
			array(
1720
				'code' => 'table',
1721
				'description' => $editortxt['table']
1722
			),
1723
			array(
1724
				'code' => 'code',
1725
				'description' => $editortxt['bbc_code']
1726
			),
1727
			array(
1728
				'code' => 'quote',
1729
				'description' => $editortxt['bbc_quote']
1730
			),
1731
			array(),
1732
			array(
1733
				'code' => 'bulletlist',
1734
				'description' => $editortxt['list_unordered']
1735
			),
1736
			array(
1737
				'code' => 'orderedlist',
1738
				'description' => $editortxt['list_ordered']
1739
			),
1740
			array(
1741
				'code' => 'horizontalrule',
1742
				'description' => $editortxt['horizontal_rule']
1743
			),
1744
		);
1745
1746
		$editor_tag_map = array(
1747
			'b' => 'bold',
1748
			'i' => 'italic',
1749
			'u' => 'underline',
1750
			's' => 'strike',
1751
			'img' => 'image',
1752
			'url' => 'link',
1753
			'sup' => 'superscript',
1754
			'sub' => 'subscript',
1755
			'hr' => 'horizontalrule',
1756
		);
1757
1758
		// Allow mods to modify BBC buttons.
1759
		// Note: pass the array here is not necessary and is deprecated, but it is kept for backward compatibility with 2.0
1760
		call_integration_hook('integrate_bbc_buttons', array(&$context['bbc_tags'], &$editor_tag_map));
1761
1762
		// Show the toggle?
1763
		if (empty($modSettings['disable_wysiwyg']))
1764
		{
1765
			$context['bbc_tags'][count($context['bbc_tags']) - 1][] = array();
1766
			$context['bbc_tags'][count($context['bbc_tags']) - 1][] = array(
1767
				'code' => 'unformat',
1768
				'description' => $editortxt['unformat_text'],
1769
			);
1770
			$context['bbc_tags'][count($context['bbc_tags']) - 1][] = array(
1771
				'code' => 'toggle',
1772
				'description' => $editortxt['toggle_view'],
1773
			);
1774
		}
1775
1776
		// Generate a list of buttons that shouldn't be shown - this should be the fastest way to do this.
1777
		$disabled_tags = array();
1778
		if (!empty($modSettings['disabledBBC']))
1779
			$disabled_tags = explode(',', $modSettings['disabledBBC']);
1780
		if (empty($modSettings['enableEmbeddedFlash']))
1781
			$disabled_tags[] = 'flash';
1782
1783
		foreach ($disabled_tags as $tag)
1784
		{
1785
			if ($tag === 'list')
1786
			{
1787
				$context['disabled_tags']['bulletlist'] = true;
1788
				$context['disabled_tags']['orderedlist'] = true;
1789
			}
1790
1791
			foreach ($editor_tag_map as $thisTag => $tagNameBBC)
1792
				if ($tag === $thisTag)
1793
					$context['disabled_tags'][$tagNameBBC] = true;
1794
1795
			$context['disabled_tags'][trim($tag)] = true;
1796
		}
1797
1798
		$bbcodes_styles = '';
1799
		$context['bbcodes_handlers'] = '';
1800
		$context['bbc_toolbar'] = array();
1801
		foreach ($context['bbc_tags'] as $row => $tagRow)
1802
		{
1803
			if (!isset($context['bbc_toolbar'][$row]))
1804
				$context['bbc_toolbar'][$row] = array();
1805
			$tagsRow = array();
1806
			foreach ($tagRow as $tag)
1807
			{
1808
				if ((!empty($tag['code'])) && empty($context['disabled_tags'][$tag['code']]))
1809
				{
1810
					$tagsRow[] = $tag['code'];
1811
					if (isset($tag['image']))
1812
						$bbcodes_styles .= '
1813
			.sceditor-button-' . $tag['code'] . ' div {
1814
				background: url(\'' . $settings['default_theme_url'] . '/images/bbc/' . $tag['image'] . '.png\');
1815
			}';
1816
					if (isset($tag['before']))
1817
					{
1818
						$context['bbcodes_handlers'] .= '
1819
				$.sceditor.command.set(
1820
					' . javaScriptEscape($tag['code']) . ', {
1821
					exec: function () {
1822
						this.wysiwygEditorInsertHtml(' . javaScriptEscape($tag['before']) . (isset($tag['after']) ? ', ' . javaScriptEscape($tag['after']) : '') . ');
1823
					},
1824
					txtExec: [' . javaScriptEscape($tag['before']) . (isset($tag['after']) ? ', ' . javaScriptEscape($tag['after']) : '') . '],
1825
					tooltip: ' . javaScriptEscape($tag['description']) . '
1826
				});';
1827
					}
1828
1829
				}
1830
				else
1831
				{
1832
					$context['bbc_toolbar'][$row][] = implode(',', $tagsRow);
1833
					$tagsRow = array();
1834
				}
1835
			}
1836
1837
			if ($row == 0)
1838
			{
1839
				$context['bbc_toolbar'][$row][] = implode(',', $tagsRow);
1840
				$tagsRow = array();
1841
				if (!isset($context['disabled_tags']['font']))
1842
					$tagsRow[] = 'font';
1843
				if (!isset($context['disabled_tags']['size']))
1844
					$tagsRow[] = 'size';
1845
				if (!isset($context['disabled_tags']['color']))
1846
					$tagsRow[] = 'color';
1847
			}
1848
			elseif ($row == 1 && empty($modSettings['disable_wysiwyg']))
1849
			{
1850
				$tmp = array();
1851
				$tagsRow[] = 'removeformat';
1852
				$tagsRow[] = 'source';
1853
				if (!empty($tmp))
1854
				{
1855
					$tagsRow[] = '|' . implode(',', $tmp);
1856
				}
1857
			}
1858
1859
			if (!empty($tagsRow))
1860
				$context['bbc_toolbar'][$row][] = implode(',', $tagsRow);
1861
		}
1862
		if (!empty($bbcodes_styles))
1863
			$context['html_headers'] .= '
1864
		<style>' . $bbcodes_styles . '
1865
		</style>';
1866
	}
1867
1868
	// Initialize smiley array... if not loaded before.
1869
	if (empty($context['smileys']) && empty($editorOptions['disable_smiley_box']))
1870
	{
1871
		$context['smileys'] = array(
1872
			'postform' => array(),
1873
			'popup' => array(),
1874
		);
1875
1876
		// Load smileys - don't bother to run a query if we're not using the database's ones anyhow.
1877
		if (empty($modSettings['smiley_enable']) && $user_info['smiley_set'] != 'none')
1878
			$context['smileys']['postform'][] = array(
1879
				'smileys' => array(
1880
					array(
1881
						'code' => ':)',
1882
						'filename' => 'smiley.gif',
1883
						'description' => $txt['icon_smiley'],
1884
					),
1885
					array(
1886
						'code' => ';)',
1887
						'filename' => 'wink.gif',
1888
						'description' => $txt['icon_wink'],
1889
					),
1890
					array(
1891
						'code' => ':D',
1892
						'filename' => 'cheesy.gif',
1893
						'description' => $txt['icon_cheesy'],
1894
					),
1895
					array(
1896
						'code' => ';D',
1897
						'filename' => 'grin.gif',
1898
						'description' => $txt['icon_grin']
1899
					),
1900
					array(
1901
						'code' => '>:(',
1902
						'filename' => 'angry.gif',
1903
						'description' => $txt['icon_angry'],
1904
					),
1905
					array(
1906
						'code' => ':(',
1907
						'filename' => 'sad.gif',
1908
						'description' => $txt['icon_sad'],
1909
					),
1910
					array(
1911
						'code' => ':o',
1912
						'filename' => 'shocked.gif',
1913
						'description' => $txt['icon_shocked'],
1914
					),
1915
					array(
1916
						'code' => '8)',
1917
						'filename' => 'cool.gif',
1918
						'description' => $txt['icon_cool'],
1919
					),
1920
					array(
1921
						'code' => '???',
1922
						'filename' => 'huh.gif',
1923
						'description' => $txt['icon_huh'],
1924
					),
1925
					array(
1926
						'code' => '::)',
1927
						'filename' => 'rolleyes.gif',
1928
						'description' => $txt['icon_rolleyes'],
1929
					),
1930
					array(
1931
						'code' => ':P',
1932
						'filename' => 'tongue.gif',
1933
						'description' => $txt['icon_tongue'],
1934
					),
1935
					array(
1936
						'code' => ':-[',
1937
						'filename' => 'embarrassed.gif',
1938
						'description' => $txt['icon_embarrassed'],
1939
					),
1940
					array(
1941
						'code' => ':-X',
1942
						'filename' => 'lipsrsealed.gif',
1943
						'description' => $txt['icon_lips'],
1944
					),
1945
					array(
1946
						'code' => ':-\\',
1947
						'filename' => 'undecided.gif',
1948
						'description' => $txt['icon_undecided'],
1949
					),
1950
					array(
1951
						'code' => ':-*',
1952
						'filename' => 'kiss.gif',
1953
						'description' => $txt['icon_kiss'],
1954
					),
1955
					array(
1956
						'code' => ':\'(',
1957
						'filename' => 'cry.gif',
1958
						'description' => $txt['icon_cry'],
1959
						'isLast' => true,
1960
					),
1961
				),
1962
				'isLast' => true,
1963
			);
1964
		elseif ($user_info['smiley_set'] != 'none')
1965
		{
1966
			if (($temp = cache_get_data('posting_smileys', 480)) == null)
1967
			{
1968
				$request = $smcFunc['db_query']('', '
1969
					SELECT code, filename, description, smiley_row, hidden
1970
					FROM {db_prefix}smileys
1971
					WHERE hidden IN (0, 2)
1972
					ORDER BY smiley_row, smiley_order',
1973
					array(
1974
					)
1975
				);
1976
				while ($row = $smcFunc['db_fetch_assoc']($request))
1977
				{
1978
					$row['filename'] = $smcFunc['htmlspecialchars']($row['filename']);
1979
					$row['description'] = $smcFunc['htmlspecialchars']($row['description']);
1980
1981
					$context['smileys'][empty($row['hidden']) ? 'postform' : 'popup'][$row['smiley_row']]['smileys'][] = $row;
1982
				}
1983
				$smcFunc['db_free_result']($request);
1984
1985
				foreach ($context['smileys'] as $section => $smileyRows)
1986
				{
1987
					foreach ($smileyRows as $rowIndex => $smileys)
1988
						$context['smileys'][$section][$rowIndex]['smileys'][count($smileys['smileys']) - 1]['isLast'] = true;
1989
1990
					if (!empty($smileyRows))
1991
						$context['smileys'][$section][count($smileyRows) - 1]['isLast'] = true;
1992
				}
1993
1994
				cache_put_data('posting_smileys', $context['smileys'], 480);
1995
			}
1996
			else
1997
				$context['smileys'] = $temp;
1998
		}
1999
	}
2000
2001
	// Set a flag so the sub template knows what to do...
2002
	$context['show_bbc'] = !empty($modSettings['enableBBC']);
2003
}
2004
2005
/**
2006
 * Create a anti-bot verification control?
2007
 * @param array &$verificationOptions Options for the verification control
2008
 * @param bool $do_test Whether to check to see if the user entered the code correctly
2009
 * @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
2010
 */
2011
function create_control_verification(&$verificationOptions, $do_test = false)
2012
{
2013
	global $modSettings, $smcFunc;
2014
	global $context, $user_info, $scripturl, $language;
2015
2016
	// First verification means we need to set up some bits...
2017
	if (empty($context['controls']['verification']))
2018
	{
2019
		// The template
2020
		loadTemplate('GenericControls');
2021
2022
		// Some javascript ma'am?
2023
		if (!empty($verificationOptions['override_visual']) || (!empty($modSettings['visual_verification_type']) && !isset($verificationOptions['override_visual'])))
2024
			loadJavaScriptFile('captcha.js', array(), 'smf_captcha');
2025
2026
		$context['use_graphic_library'] = in_array('gd', get_loaded_extensions());
2027
2028
		// Skip I, J, L, O, Q, S and Z.
2029
		$context['standard_captcha_range'] = array_merge(range('A', 'H'), array('K', 'M', 'N', 'P', 'R'), range('T', 'Y'));
2030
	}
2031
2032
	// Always have an ID.
2033
	assert(isset($verificationOptions['id']));
2034
	$isNew = !isset($context['controls']['verification'][$verificationOptions['id']]);
2035
2036
	// Log this into our collection.
2037
	if ($isNew)
2038
		$context['controls']['verification'][$verificationOptions['id']] = array(
2039
			'id' => $verificationOptions['id'],
2040
			'empty_field' => empty($verificationOptions['no_empty_field']),
2041
			'show_visual' => !empty($verificationOptions['override_visual']) || (!empty($modSettings['visual_verification_type']) && !isset($verificationOptions['override_visual'])),
2042
			'number_questions' => isset($verificationOptions['override_qs']) ? $verificationOptions['override_qs'] : (!empty($modSettings['qa_verification_number']) ? $modSettings['qa_verification_number'] : 0),
2043
			'max_errors' => isset($verificationOptions['max_errors']) ? $verificationOptions['max_errors'] : 3,
2044
			'image_href' => $scripturl . '?action=verificationcode;vid=' . $verificationOptions['id'] . ';rand=' . md5(mt_rand()),
2045
			'text_value' => '',
2046
			'questions' => array(),
2047
			'can_recaptcha' => !empty($modSettings['recaptcha_enabled']) && !empty($modSettings['recaptcha_site_key']) && !empty($modSettings['recaptcha_secret_key']),
2048
		);
2049
	$thisVerification = &$context['controls']['verification'][$verificationOptions['id']];
2050
2051
	// Is there actually going to be anything?
2052
	if (empty($thisVerification['show_visual']) && empty($thisVerification['number_questions']) && empty($thisVerification['can_recaptcha']))
2053
		return false;
2054
	elseif (!$isNew && !$do_test)
2055
		return true;
2056
2057
	// Sanitize reCAPTCHA fields?
2058
	if ($thisVerification['can_recaptcha'])
2059
	{
2060
		// Only allow 40 alphanumeric, underscore and dash characters.
2061
		$thisVerification['recaptcha_site_key'] = preg_replace('/(0-9a-zA-Z_){40}/', '$1', $modSettings['recaptcha_site_key']);
2062
2063
		// Light or dark theme...
2064
		$thisVerification['recaptcha_theme'] = preg_replace('/(light|dark)/', '$1', $modSettings['recaptcha_theme']);
2065
	}
2066
2067
	// Add javascript for the object.
2068
	if ($context['controls']['verification'][$verificationOptions['id']]['show_visual'])
2069
		$context['insert_after_template'] .= '
2070
			<script>
2071
				var verification' . $verificationOptions['id'] . 'Handle = new smfCaptcha("' . $thisVerification['image_href'] . '", "' . $verificationOptions['id'] . '", ' . ($context['use_graphic_library'] ? 1 : 0) . ');
2072
			</script>';
2073
2074
	// If we want questions do we have a cache of all the IDs?
2075
	if (!empty($thisVerification['number_questions']) && empty($modSettings['question_id_cache']))
2076
	{
2077
		if (($modSettings['question_id_cache'] = cache_get_data('verificationQuestions', 300)) == null)
2078
		{
2079
			$request = $smcFunc['db_query']('', '
2080
				SELECT id_question, lngfile, question, answers
2081
				FROM {db_prefix}qanda',
2082
				array()
2083
			);
2084
			$modSettings['question_id_cache'] = array(
2085
				'questions' => array(),
2086
				'langs' => array(),
2087
			);
2088
			// This is like Captain Kirk climbing a mountain in some ways. This is L's fault, mkay? :P
2089
			while ($row = $smcFunc['db_fetch_assoc']($request))
2090
			{
2091
				$id_question = $row['id_question'];
2092
				unset ($row['id_question']);
2093
				// Make them all lowercase. We can't directly use $smcFunc['strtolower'] with array_walk, so do it manually, eh?
2094
				$row['answers'] = $smcFunc['json_decode']($row['answers'], true);
2095
				foreach ($row['answers'] as $k => $v)
2096
					$row['answers'][$k] = $smcFunc['strtolower']($v);
2097
2098
				$modSettings['question_id_cache']['questions'][$id_question] = $row;
2099
				$modSettings['question_id_cache']['langs'][$row['lngfile']][] = $id_question;
2100
			}
2101
			$smcFunc['db_free_result']($request);
2102
2103
			cache_put_data('verificationQuestions', $modSettings['question_id_cache'], 300);
2104
		}
2105
	}
2106
2107
	if (!isset($_SESSION[$verificationOptions['id'] . '_vv']))
2108
		$_SESSION[$verificationOptions['id'] . '_vv'] = array();
2109
2110
	// Do we need to refresh the verification?
2111
	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']))
2112
		$force_refresh = true;
2113
	else
2114
		$force_refresh = false;
2115
2116
	// This can also force a fresh, although unlikely.
2117 View Code Duplication
	if (($thisVerification['show_visual'] && empty($_SESSION[$verificationOptions['id'] . '_vv']['code'])) || ($thisVerification['number_questions'] && empty($_SESSION[$verificationOptions['id'] . '_vv']['q'])))
2118
		$force_refresh = true;
2119
2120
	$verification_errors = array();
2121
	// Start with any testing.
2122
	if ($do_test)
2123
	{
2124
		// This cannot happen!
2125
		if (!isset($_SESSION[$verificationOptions['id'] . '_vv']['count']))
2126
			fatal_lang_error('no_access', false);
2127
		// ... nor this!
2128
		if ($thisVerification['number_questions'] && (!isset($_SESSION[$verificationOptions['id'] . '_vv']['q']) || !isset($_REQUEST[$verificationOptions['id'] . '_vv']['q'])))
2129
			fatal_lang_error('no_access', false);
2130
		// Hmm, it's requested but not actually declared. This shouldn't happen.
2131
		if ($thisVerification['empty_field'] && empty($_SESSION[$verificationOptions['id'] . '_vv']['empty_field']))
2132
			fatal_lang_error('no_access', false);
2133
		// While we're here, did the user do something bad?
2134 View Code Duplication
		if ($thisVerification['empty_field'] && !empty($_SESSION[$verificationOptions['id'] . '_vv']['empty_field']) && !empty($_REQUEST[$_SESSION[$verificationOptions['id'] . '_vv']['empty_field']]))
2135
			$verification_errors[] = 'wrong_verification_answer';
2136
2137
		if ($thisVerification['can_recaptcha'])
2138
		{
2139
			$reCaptcha = new \ReCaptcha\ReCaptcha($modSettings['recaptcha_secret_key']);
2140
2141
			// Was there a reCAPTCHA response?
2142
			if (isset($_POST['g-recaptcha-response']))
2143
			{
2144
				$resp = $reCaptcha->verify($_POST['g-recaptcha-response'], $user_info['ip']);
2145
2146
				if (!$resp->isSuccess())
2147
					$verification_errors[] = 'wrong_verification_code';
2148
			}
2149
			else
2150
				$verification_errors[] = 'wrong_verification_code';
2151
		}
2152
		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']))
2153
			$verification_errors[] = 'wrong_verification_code';
2154
		if ($thisVerification['number_questions'])
2155
		{
2156
			$incorrectQuestions = array();
2157
			foreach ($_SESSION[$verificationOptions['id'] . '_vv']['q'] as $q)
2158
			{
2159
				// We don't have this question any more, thus no answers.
2160
				if (!isset($modSettings['question_id_cache']['questions'][$q]))
2161
					continue;
2162
				// This is quite complex. We have our question but it might have multiple answers.
2163
				// First, did they actually answer this question?
2164
				if (!isset($_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q]) || trim($_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q]) == '')
2165
				{
2166
					$incorrectQuestions[] = $q;
2167
					continue;
2168
				}
2169
				// Second, is their answer in the list of possible answers?
2170
				else
2171
				{
2172
					$given_answer = trim($smcFunc['htmlspecialchars'](strtolower($_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q])));
2173
					if (!in_array($given_answer, $modSettings['question_id_cache']['questions'][$q]['answers']))
2174
						$incorrectQuestions[] = $q;
2175
				}
2176
			}
2177
2178
			if (!empty($incorrectQuestions))
2179
				$verification_errors[] = 'wrong_verification_answer';
2180
		}
2181
	}
2182
2183
	// Any errors means we refresh potentially.
2184
	if (!empty($verification_errors))
2185
	{
2186
		if (empty($_SESSION[$verificationOptions['id'] . '_vv']['errors']))
2187
			$_SESSION[$verificationOptions['id'] . '_vv']['errors'] = 0;
2188
		// Too many errors?
2189
		elseif ($_SESSION[$verificationOptions['id'] . '_vv']['errors'] > $thisVerification['max_errors'])
2190
			$force_refresh = true;
2191
2192
		// Keep a track of these.
2193
		$_SESSION[$verificationOptions['id'] . '_vv']['errors']++;
2194
	}
2195
2196
	// Are we refreshing then?
2197
	if ($force_refresh)
2198
	{
2199
		// Assume nothing went before.
2200
		$_SESSION[$verificationOptions['id'] . '_vv']['count'] = 0;
2201
		$_SESSION[$verificationOptions['id'] . '_vv']['errors'] = 0;
2202
		$_SESSION[$verificationOptions['id'] . '_vv']['did_pass'] = false;
2203
		$_SESSION[$verificationOptions['id'] . '_vv']['q'] = array();
2204
		$_SESSION[$verificationOptions['id'] . '_vv']['code'] = '';
2205
2206
		// Make our magic empty field.
2207
		if ($thisVerification['empty_field'])
2208
		{
2209
			// 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.
2210
			$terms = array('gadget', 'device', 'uid', 'gid', 'guid', 'uuid', 'unique', 'identifier');
2211
			$second_terms = array('hash', 'cipher', 'code', 'key', 'unlock', 'bit', 'value');
2212
			$start = mt_rand(0, 27);
2213
			$hash = substr(md5(time()), $start, 4);
2214
			$_SESSION[$verificationOptions['id'] . '_vv']['empty_field'] = $terms[array_rand($terms)] . '-' . $second_terms[array_rand($second_terms)] . '-' . $hash;
2215
		}
2216
2217
		// Generating a new image.
2218
		if ($thisVerification['show_visual'])
2219
		{
2220
			// Are we overriding the range?
2221
			$character_range = !empty($verificationOptions['override_range']) ? $verificationOptions['override_range'] : $context['standard_captcha_range'];
2222
2223
			for ($i = 0; $i < 6; $i++)
2224
				$_SESSION[$verificationOptions['id'] . '_vv']['code'] .= $character_range[array_rand($character_range)];
2225
		}
2226
2227
		// Getting some new questions?
2228
		if ($thisVerification['number_questions'])
2229
		{
2230
			// Attempt to try the current page's language, followed by the user's preference, followed by the site default.
2231
			$possible_langs = array();
2232
			if (isset($_SESSION['language']))
2233
				$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...
2234
			if (!empty($user_info['language']));
2235
			$possible_langs[] = $user_info['language'];
2236
			$possible_langs[] = $language;
2237
2238
			$questionIDs = array();
2239
			foreach ($possible_langs as $lang)
2240
			{
2241
				$lang = strtr($lang, array('-utf8' => ''));
2242
				if (isset($modSettings['question_id_cache']['langs'][$lang]))
2243
				{
2244
					// If we find questions for this, grab the ids from this language's ones, randomize the array and take just the number we need.
2245
					$questionIDs = $modSettings['question_id_cache']['langs'][$lang];
2246
					shuffle($questionIDs);
2247
					$questionIDs = array_slice($questionIDs, 0, $thisVerification['number_questions']);
2248
					break;
2249
				}
2250
			}
2251
		}
2252
	}
2253
	else
2254
	{
2255
		// Same questions as before.
2256
		$questionIDs = !empty($_SESSION[$verificationOptions['id'] . '_vv']['q']) ? $_SESSION[$verificationOptions['id'] . '_vv']['q'] : array();
2257
		$thisVerification['text_value'] = !empty($_REQUEST[$verificationOptions['id'] . '_vv']['code']) ? $smcFunc['htmlspecialchars']($_REQUEST[$verificationOptions['id'] . '_vv']['code']) : '';
2258
	}
2259
2260
	// If we do have an empty field, it would be nice to hide it from legitimate users who shouldn't be populating it anyway.
2261
	if (!empty($_SESSION[$verificationOptions['id'] . '_vv']['empty_field']))
2262
	{
2263
		if (!isset($context['html_headers']))
2264
			$context['html_headers'] = '';
2265
		$context['html_headers'] .= '<style>.vv_special { display:none; }</style>';
2266
	}
2267
2268
	// Have we got some questions to load?
2269
	if (!empty($questionIDs))
2270
	{
2271
		$_SESSION[$verificationOptions['id'] . '_vv']['q'] = array();
2272
		foreach ($questionIDs as $q)
2273
		{
2274
			// Bit of a shortcut this.
2275
			$row = &$modSettings['question_id_cache']['questions'][$q];
2276
			$thisVerification['questions'][] = array(
2277
				'id' => $q,
2278
				'q' => parse_bbc($row['question']),
2279
				'is_error' => !empty($incorrectQuestions) && in_array($q, $incorrectQuestions),
2280
				// Remember a previous submission?
2281
				'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]) : '',
2282
			);
2283
			$_SESSION[$verificationOptions['id'] . '_vv']['q'][] = $q;
2284
		}
2285
	}
2286
2287
	$_SESSION[$verificationOptions['id'] . '_vv']['count'] = empty($_SESSION[$verificationOptions['id'] . '_vv']['count']) ? 1 : $_SESSION[$verificationOptions['id'] . '_vv']['count'] + 1;
2288
2289
	// Return errors if we have them.
2290
	if (!empty($verification_errors))
2291
		return $verification_errors;
2292
	// If we had a test that one, make a note.
2293
	elseif ($do_test)
2294
		$_SESSION[$verificationOptions['id'] . '_vv']['did_pass'] = true;
2295
2296
	// Say that everything went well chaps.
2297
	return true;
2298
}
2299
2300
/**
2301
 * This keeps track of all registered handling functions for auto suggest functionality and passes execution to them.
2302
 * @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...
2303
 * @return void|bool Returns whether the callback function is registered if $checkRegistered isn't null
2304
 */
2305
function AutoSuggestHandler($checkRegistered = null)
2306
{
2307
	global $smcFunc, $context;
2308
2309
	// These are all registered types.
2310
	$searchTypes = array(
2311
		'member' => 'Member',
2312
		'membergroups' => 'MemberGroups',
2313
		'versions' => 'SMFVersions',
2314
	);
2315
2316
	call_integration_hook('integrate_autosuggest', array(&$searchTypes));
2317
2318
	// If we're just checking the callback function is registered return true or false.
2319
	if ($checkRegistered != null)
2320
		return isset($searchTypes[$checkRegistered]) && function_exists('AutoSuggest_Search_' . $checkRegistered);
2321
2322
	checkSession('get');
2323
	loadTemplate('Xml');
2324
2325
	// Any parameters?
2326
	$context['search_param'] = isset($_REQUEST['search_param']) ? $smcFunc['json_decode'](base64_decode($_REQUEST['search_param']), true) : array();
2327
2328
	if (isset($_REQUEST['suggest_type'], $_REQUEST['search']) && isset($searchTypes[$_REQUEST['suggest_type']]))
2329
	{
2330
		$function = 'AutoSuggest_Search_' . $searchTypes[$_REQUEST['suggest_type']];
2331
		$context['sub_template'] = 'generic_xml';
2332
		$context['xml_data'] = $function();
2333
	}
2334
}
2335
2336
/**
2337
 * Search for a member - by real_name or member_name by default.
2338
 *
2339
 * @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...
2340
 */
2341
function AutoSuggest_Search_Member()
2342
{
2343
	global $user_info, $smcFunc, $context;
2344
2345
	$_REQUEST['search'] = trim($smcFunc['strtolower']($_REQUEST['search'])) . '*';
2346
	$_REQUEST['search'] = strtr($_REQUEST['search'], array('%' => '\%', '_' => '\_', '*' => '%', '?' => '_', '&#038;' => '&amp;'));
2347
2348
	// Find the member.
2349
	$request = $smcFunc['db_query']('', '
2350
		SELECT id_member, real_name
2351
		FROM {db_prefix}members
2352
		WHERE {raw:real_name} LIKE {string:search}' . (!empty($context['search_param']['buddies']) ? '
2353
			AND id_member IN ({array_int:buddy_list})' : '') . '
2354
			AND is_activated IN (1, 11)
2355
		LIMIT ' . ($smcFunc['strlen']($_REQUEST['search']) <= 2 ? '100' : '800'),
2356
		array(
2357
			'real_name' => $smcFunc['db_case_sensitive'] ? 'LOWER(real_name)' : 'real_name',
2358
			'buddy_list' => $user_info['buddies'],
2359
			'search' => $_REQUEST['search'],
2360
		)
2361
	);
2362
	$xml_data = array(
2363
		'items' => array(
2364
			'identifier' => 'item',
2365
			'children' => array(),
2366
		),
2367
	);
2368 View Code Duplication
	while ($row = $smcFunc['db_fetch_assoc']($request))
2369
	{
2370
		$row['real_name'] = strtr($row['real_name'], array('&amp;' => '&#038;', '&lt;' => '&#060;', '&gt;' => '&#062;', '&quot;' => '&#034;'));
2371
2372
		$xml_data['items']['children'][] = array(
2373
			'attributes' => array(
2374
				'id' => $row['id_member'],
2375
			),
2376
			'value' => $row['real_name'],
2377
		);
2378
	}
2379
	$smcFunc['db_free_result']($request);
2380
2381
	return $xml_data;
2382
}
2383
2384
/**
2385
 * Search for a membergroup by name
2386
 *
2387
 * @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...
2388
 */
2389
function AutoSuggest_Search_MemberGroups()
2390
{
2391
	global $smcFunc;
2392
2393
	$_REQUEST['search'] = trim($smcFunc['strtolower']($_REQUEST['search'])) . '*';
2394
	$_REQUEST['search'] = strtr($_REQUEST['search'], array('%' => '\%', '_' => '\_', '*' => '%', '?' => '_', '&#038;' => '&amp;'));
2395
2396
	// Find the group.
2397
	// Only return groups which are not post-based and not "Hidden", but not the "Administrators" or "Moderators" groups.
2398
	$request = $smcFunc['db_query']('', '
2399
		SELECT id_group, group_name
2400
		FROM {db_prefix}membergroups
2401
		WHERE {raw:group_name} LIKE {string:search}
2402
			AND min_posts = {int:min_posts}
2403
			AND id_group NOT IN ({array_int:invalid_groups})
2404
			AND hidden != {int:hidden}
2405
		',
2406
		array(
2407
			'group_name' => $smcFunc['db_case_sensitive'] ? 'LOWER(group_name}' : 'group_name',
2408
			'min_posts' => -1,
2409
			'invalid_groups' => array(1, 3),
2410
			'hidden' => 2,
2411
			'search' => $_REQUEST['search'],
2412
		)
2413
	);
2414
	$xml_data = array(
2415
		'items' => array(
2416
			'identifier' => 'item',
2417
			'children' => array(),
2418
		),
2419
	);
2420 View Code Duplication
	while ($row = $smcFunc['db_fetch_assoc']($request))
2421
	{
2422
		$row['group_name'] = strtr($row['group_name'], array('&amp;' => '&#038;', '&lt;' => '&#060;', '&gt;' => '&#062;', '&quot;' => '&#034;'));
2423
2424
		$xml_data['items']['children'][] = array(
2425
			'attributes' => array(
2426
				'id' => $row['id_group'],
2427
			),
2428
			'value' => $row['group_name'],
2429
		);
2430
	}
2431
	$smcFunc['db_free_result']($request);
2432
2433
	return $xml_data;
2434
}
2435
2436
/**
2437
 * Provides a list of possible SMF versions to use in emulation
2438
 *
2439
 * @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...
2440
 */
2441
function AutoSuggest_Search_SMFVersions()
2442
{
2443
	global $smcFunc;
2444
2445
	$xml_data = array(
2446
		'items' => array(
2447
			'identifier' => 'item',
2448
			'children' => array(),
2449
		),
2450
	);
2451
2452
	// First try and get it from the database.
2453
	$versions = array();
2454
	$request = $smcFunc['db_query']('', '
2455
		SELECT data
2456
		FROM {db_prefix}admin_info_files
2457
		WHERE filename = {string:latest_versions}
2458
			AND path = {string:path}',
2459
		array(
2460
			'latest_versions' => 'latest-versions.txt',
2461
			'path' => '/smf/',
2462
		)
2463
	);
2464
	if (($smcFunc['db_num_rows']($request) > 0) && ($row = $smcFunc['db_fetch_assoc']($request)) && !empty($row['data']))
2465
	{
2466
		// The file can be either Windows or Linux line endings, but let's ensure we clean it as best we can.
2467
		$possible_versions = explode("\n", $row['data']);
2468
		foreach ($possible_versions as $ver)
2469
		{
2470
			$ver = trim($ver);
2471
			if (strpos($ver, 'SMF') === 0)
2472
				$versions[] = $ver;
2473
		}
2474
	}
2475
	$smcFunc['db_free_result']($request);
2476
2477
	// Just in case we don't have ANYthing.
2478
	if (empty($versions))
2479
		$versions = array('SMF 2.0');
2480
2481
	foreach ($versions as $id => $version)
2482
		if (strpos($version, strtoupper($_REQUEST['search'])) !== false)
2483
			$xml_data['items']['children'][] = array(
2484
				'attributes' => array(
2485
					'id' => $id,
2486
				),
2487
				'value' => $version,
2488
			);
2489
2490
	return $xml_data;
2491
}
2492
2493
?>