Issues (1014)

Sources/Subs-Editor.php (7 issues)

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 https://www.simplemachines.org
11
 * @copyright 2022 Simple Machines and individual contributors
12
 * @license https://www.simplemachines.org/about/smf/license.php BSD
13
 *
14
 * @version 2.1.2
15
 */
16
17
if (!defined('SMF'))
18
	die('No direct access...');
19
20
/**
21
 * As of SMF 2.1, this is unused. But it is available if any mod wants to use it.
22
 * Convert only the BBC that can be edited in HTML mode for the (old) editor.
23
 *
24
 * @deprecated since version 2.1
25
 * @param string $text The text with bbcode in it
26
 * @param boolean $compat_mode Whether to actually convert the text
27
 * @return string The text
28
 */
29
function bbc_to_html($text, $compat_mode = false)
30
{
31
	global $modSettings;
32
33
	if (!$compat_mode)
34
		return $text;
35
36
	// Turn line breaks back into br's.
37
	$text = strtr($text, array("\r" => '', "\n" => '<br>'));
38
39
	// Prevent conversion of all bbcode inside these bbcodes.
40
	// @todo Tie in with bbc permissions ?
41
	foreach (array('code', 'php', 'nobbc') as $code)
42
	{
43
		if (strpos($text, '[' . $code) !== false)
44
		{
45
			$parts = preg_split('~(\[/' . $code . '\]|\[' . $code . '(?:=[^\]]+)?\])~i', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
46
47
			// Only mess with stuff inside tags.
48
			for ($i = 0, $n = count($parts); $i < $n; $i++)
49
			{
50
				// Value of 2 means we're inside the tag.
51
				if ($i % 4 == 2)
52
					$parts[$i] = strtr($parts[$i], array('[' => '&#91;', ']' => '&#93;', "'" => "'"));
53
			}
54
			// Put our humpty dumpty message back together again.
55
			$text = implode('', $parts);
56
		}
57
	}
58
59
	// What tags do we allow?
60
	$allowed_tags = array('b', 'u', 'i', 's', 'hr', 'list', 'li', 'font', 'size', 'color', 'img', 'left', 'center', 'right', 'url', 'email', 'ftp', 'sub', 'sup');
61
62
	$text = parse_bbc($text, true, '', $allowed_tags);
63
64
	// Fix for having a line break then a thingy.
65
	$text = strtr($text, array('<br><div' => '<div', "\n" => '', "\r" => ''));
66
67
	// Note that IE doesn't understand spans really - make them something "legacy"
68
	$working_html = array(
69
		'~<del>(.+?)</del>~i' => '<strike>$1</strike>',
70
		'~<span\sclass="bbc_u">(.+?)</span>~i' => '<u>$1</u>',
71
		'~<span\sstyle="color:\s*([#\d\w]+);" class="bbc_color">(.+?)</span>~i' => '<font color="$1">$2</font>',
72
		'~<span\sstyle="font-family:\s*([#\d\w\s]+);" class="bbc_font">(.+?)</span>~i' => '<font face="$1">$2</font>',
73
		'~<div\sstyle="text-align:\s*(left|right);">(.+?)</div>~i' => '<p align="$1">$2</p>',
74
	);
75
	$text = preg_replace(array_keys($working_html), array_values($working_html), $text);
76
77
	// Parse unique ID's and disable javascript into the smileys - using the double space.
78
	$i = 1;
79
	$text = preg_replace_callback(
80
		'~(?:\s|&nbsp;)?<(img\ssrc="' . preg_quote($modSettings['smileys_url'], '~') . '/[^<>]+?/([^<>]+?)"\s*)[^<>]*?class="smiley">~',
81
		function($m) use (&$i)
82
		{
83
			return '<' . stripslashes($m[1]) . 'alt="" title="" onresizestart="return false;" id="smiley_' . $i++ . '_' . $m[2] . '" style="padding: 0 3px 0 3px;">';
84
		},
85
		$text
86
	);
87
88
	return $text;
89
}
90
91
/**
92
 * Converts HTML to BBC
93
 * As of SMF 2.1, only used by ManageBoards.php (and possibly mods)
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\b[^>]+alt="([^"]+)"[^>]+class="smiley"[^>]*>(?:\s)?~i', $text, $matches);
141
	if (!empty($matches[0]))
142
	{
143
		// Get all our smiley codes
144
		$request = $smcFunc['db_query']('', '
145
			SELECT code
146
			FROM {db_prefix}smileys
147
			ORDER BY LENGTH(code) DESC',
148
			array()
149
		);
150
		$smiley_codes = $smcFunc['db_fetch_all']($request);
151
		$smcFunc['db_free_result']($request);
152
153
		foreach ($matches[1] as $k => $possible_code)
154
		{
155
			$possible_code = un_htmlspecialchars($possible_code);
156
157
			if (in_array($possible_code, $smiley_codes))
158
				$matches[1][$k] = '-[]-smf_smily_start#|#' . $possible_code . '-[]-smf_smily_end#|#';
159
			else
160
				$matches[1][$k] = $matches[0][$k];
161
		}
162
163
		// Replace the tags!
164
		$text = str_replace($matches[0], $matches[1], $text);
165
166
		// Now sort out spaces
167
		$text = str_replace(array('-[]-smf_smily_end#|#-[]-smf_smily_start#|#', '-[]-smf_smily_end#|#', '-[]-smf_smily_start#|#'), ' ', $text);
168
	}
169
170
	// Only try to buy more time if the client didn't quit.
171
	if (connection_aborted() && $context['server']['is_apache'])
172
		@apache_reset_timeout();
173
174
	$parts = preg_split('~(<[A-Za-z]+\s*[^<>]*?style="?[^<>"]+"?[^<>]*?(?:/?)>|</[A-Za-z]+>)~', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
175
	$replacement = '';
176
	$stack = array();
177
178
	foreach ($parts as $part)
179
	{
180
		if (preg_match('~(<([A-Za-z]+)\s*[^<>]*?)style="?([^<>"]+)"?([^<>]*?(/?)>)~', $part, $matches) === 1)
181
		{
182
			// If it's being closed instantly, we can't deal with it...yet.
183
			if ($matches[5] === '/')
184
				continue;
185
			else
186
			{
187
				// Get an array of styles that apply to this element. (The strtr is there to combat HTML generated by Word.)
188
				$styles = explode(';', strtr($matches[3], array('&quot;' => '')));
189
				$curElement = $matches[2];
190
				$precedingStyle = $matches[1];
191
				$afterStyle = $matches[4];
192
				$curCloseTags = '';
193
				$extra_attr = '';
194
195
				foreach ($styles as $type_value_pair)
196
				{
197
					// Remove spaces and convert uppercase letters.
198
					$clean_type_value_pair = strtolower(strtr(trim($type_value_pair), '=', ':'));
199
200
					// Something like 'font-weight: bold' is expected here.
201
					if (strpos($clean_type_value_pair, ':') === false)
202
						continue;
203
204
					// Capture the elements of a single style item (e.g. 'font-weight' and 'bold').
205
					list ($style_type, $style_value) = explode(':', $type_value_pair);
206
207
					$style_value = trim($style_value);
208
209
					switch (trim($style_type))
210
					{
211
						case 'font-weight':
212
							if ($style_value === 'bold')
213
							{
214
								$curCloseTags .= '[/b]';
215
								$replacement .= '[b]';
216
							}
217
							break;
218
219
						case 'text-decoration':
220
							if ($style_value == 'underline')
221
							{
222
								$curCloseTags .= '[/u]';
223
								$replacement .= '[u]';
224
							}
225
							elseif ($style_value == 'line-through')
226
							{
227
								$curCloseTags .= '[/s]';
228
								$replacement .= '[s]';
229
							}
230
							break;
231
232
						case 'text-align':
233
							if ($style_value == 'left')
234
							{
235
								$curCloseTags .= '[/left]';
236
								$replacement .= '[left]';
237
							}
238
							elseif ($style_value == 'center')
239
							{
240
								$curCloseTags .= '[/center]';
241
								$replacement .= '[center]';
242
							}
243
							elseif ($style_value == 'right')
244
							{
245
								$curCloseTags .= '[/right]';
246
								$replacement .= '[right]';
247
							}
248
							break;
249
250
						case 'font-style':
251
							if ($style_value == 'italic')
252
							{
253
								$curCloseTags .= '[/i]';
254
								$replacement .= '[i]';
255
							}
256
							break;
257
258
						case 'color':
259
							$curCloseTags .= '[/color]';
260
							$replacement .= '[color=' . $style_value . ']';
261
							break;
262
263
						case 'font-size':
264
							// Sometimes people put decimals where decimals should not be.
265
							if (preg_match('~(\d)+\.\d+(p[xt])~i', $style_value, $dec_matches) === 1)
266
								$style_value = $dec_matches[1] . $dec_matches[2];
267
268
							$curCloseTags .= '[/size]';
269
							$replacement .= '[size=' . $style_value . ']';
270
							break;
271
272
						case 'font-family':
273
							// Only get the first freaking font if there's a list!
274
							if (strpos($style_value, ',') !== false)
275
								$style_value = substr($style_value, 0, strpos($style_value, ','));
276
277
							$curCloseTags .= '[/font]';
278
							$replacement .= '[font=' . strtr($style_value, array("'" => '')) . ']';
279
							break;
280
281
						// This is a hack for images with dimensions embedded.
282
						case 'width':
283
						case 'height':
284
							if (preg_match('~[1-9]\d*~i', $style_value, $dimension) === 1)
285
								$extra_attr .= ' ' . $style_type . '="' . $dimension[0] . '"';
286
							break;
287
288
						case 'list-style-type':
289
							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)
290
								$extra_attr .= ' listtype="' . $listType[0] . '"';
291
							break;
292
					}
293
				}
294
295
				// Preserve some tags stripping the styling.
296
				if (in_array($matches[2], array('a', 'font', 'td')))
297
				{
298
					$replacement .= $precedingStyle . $afterStyle;
299
					$curCloseTags = '</' . $matches[2] . '>' . $curCloseTags;
300
				}
301
302
				// If there's something that still needs closing, push it to the stack.
303
				if (!empty($curCloseTags))
304
					array_push($stack, array(
305
							'element' => strtolower($curElement),
306
							'closeTags' => $curCloseTags
307
						)
308
					);
309
				elseif (!empty($extra_attr))
310
					$replacement .= $precedingStyle . $extra_attr . $afterStyle;
311
			}
312
		}
313
314
		elseif (preg_match('~</([A-Za-z]+)>~', $part, $matches) === 1)
315
		{
316
			// Is this the element that we've been waiting for to be closed?
317
			if (!empty($stack) && strtolower($matches[1]) === $stack[count($stack) - 1]['element'])
318
			{
319
				$byebyeTag = array_pop($stack);
320
				$replacement .= $byebyeTag['closeTags'];
321
			}
322
323
			// Must've been something else.
324
			else
325
				$replacement .= $part;
326
		}
327
		// In all other cases, just add the part to the replacement.
328
		else
329
			$replacement .= $part;
330
	}
331
332
	// Now put back the replacement in the text.
333
	$text = $replacement;
334
335
	// We are not finished yet, request more time.
336
	if (connection_aborted() && $context['server']['is_apache'])
337
		@apache_reset_timeout();
338
339
	// Let's pull out any legacy alignments.
340
	while (preg_match('~<([A-Za-z]+)\s+[^<>]*?(align="*(left|center|right)"*)[^<>]*?(/?)>~i', $text, $matches) === 1)
341
	{
342
		// Find the position in the text of this tag over again.
343
		$start_pos = strpos($text, $matches[0]);
344
		if ($start_pos === false)
345
			break;
346
347
		// End tag?
348
		if ($matches[4] != '/' && strpos($text, '</' . $matches[1] . '>', $start_pos) !== false)
349
		{
350
			$end_pos = strpos($text, '</' . $matches[1] . '>', $start_pos);
351
352
			// Remove the align from that tag so it's never checked again.
353
			$tag = substr($text, $start_pos, strlen($matches[0]));
354
			$content = substr($text, $start_pos + strlen($matches[0]), $end_pos - $start_pos - strlen($matches[0]));
355
			$tag = str_replace($matches[2], '', $tag);
356
357
			// Put the tags back into the body.
358
			$text = substr($text, 0, $start_pos) . $tag . '[' . $matches[3] . ']' . $content . '[/' . $matches[3] . ']' . substr($text, $end_pos);
359
		}
360
		else
361
		{
362
			// Just get rid of this evil tag.
363
			$text = substr($text, 0, $start_pos) . substr($text, $start_pos + strlen($matches[0]));
364
		}
365
	}
366
367
	// Let's do some special stuff for fonts - cause we all love fonts.
368
	while (preg_match('~<font\s+([^<>]*)>~i', $text, $matches) === 1)
369
	{
370
		// Find the position of this again.
371
		$start_pos = strpos($text, $matches[0]);
372
		$end_pos = false;
373
		if ($start_pos === false)
374
			break;
375
376
		// This must have an end tag - and we must find the right one.
377
		$lower_text = strtolower($text);
378
379
		$start_pos_test = $start_pos + 4;
380
		// How many starting tags must we find closing ones for first?
381
		$start_font_tag_stack = 0;
382
		while ($start_pos_test < strlen($text))
383
		{
384
			// Where is the next starting font?
385
			$next_start_pos = strpos($lower_text, '<font', $start_pos_test);
386
			$next_end_pos = strpos($lower_text, '</font>', $start_pos_test);
387
388
			// Did we past another starting tag before an end one?
389
			if ($next_start_pos !== false && $next_start_pos < $next_end_pos)
390
			{
391
				$start_font_tag_stack++;
392
				$start_pos_test = $next_start_pos + 4;
393
			}
394
			// Otherwise we have an end tag but not the right one?
395
			elseif ($start_font_tag_stack)
396
			{
397
				$start_font_tag_stack--;
398
				$start_pos_test = $next_end_pos + 4;
399
			}
400
			// Otherwise we're there!
401
			else
402
			{
403
				$end_pos = $next_end_pos;
404
				break;
405
			}
406
		}
407
		if ($end_pos === false)
408
			break;
409
410
		// Now work out what the attributes are.
411
		$attribs = fetchTagAttributes($matches[1]);
412
		$tags = array();
413
		$sizes_equivalence = array(1 => '8pt', '10pt', '12pt', '14pt', '18pt', '24pt', '36pt');
414
		foreach ($attribs as $s => $v)
415
		{
416
			if ($s == 'size')
417
			{
418
				// Cast before empty chech because casting a string results in a 0 and we don't have zeros in the array! ;)
419
				$v = (int) trim($v);
420
				$v = empty($v) ? 1 : $v;
421
				$tags[] = array('[size=' . $sizes_equivalence[$v] . ']', '[/size]');
422
			}
423
			elseif ($s == 'face')
424
				$tags[] = array('[font=' . trim(strtolower($v)) . ']', '[/font]');
425
			elseif ($s == 'color')
426
				$tags[] = array('[color=' . trim(strtolower($v)) . ']', '[/color]');
427
		}
428
429
		// As before add in our tags.
430
		$before = $after = '';
431
		foreach ($tags as $tag)
432
		{
433
			$before .= $tag[0];
434
			if (isset($tag[1]))
435
				$after = $tag[1] . $after;
436
		}
437
438
		// Remove the tag so it's never checked again.
439
		$content = substr($text, $start_pos + strlen($matches[0]), $end_pos - $start_pos - strlen($matches[0]));
440
441
		// Put the tags back into the body.
442
		$text = substr($text, 0, $start_pos) . $before . $content . $after . substr($text, $end_pos + 7);
443
	}
444
445
	// Almost there, just a little more time.
446
	if (connection_aborted() && $context['server']['is_apache'])
447
		@apache_reset_timeout();
448
449
	if (count($parts = preg_split('~<(/?)(li|ol|ul)([^>]*)>~i', $text, -1, PREG_SPLIT_DELIM_CAPTURE)) > 1)
450
	{
451
		// A toggle that dermines whether we're directly under a <ol> or <ul>.
452
		$inList = false;
453
454
		// Keep track of the number of nested list levels.
455
		$listDepth = 0;
456
457
		// Map what we can expect from the HTML to what is supported by SMF.
458
		$listTypeMapping = array(
459
			'1' => 'decimal',
460
			'A' => 'upper-alpha',
461
			'a' => 'lower-alpha',
462
			'I' => 'upper-roman',
463
			'i' => 'lower-roman',
464
			'disc' => 'disc',
465
			'square' => 'square',
466
			'circle' => 'circle',
467
		);
468
469
		// $i: text, $i + 1: '/', $i + 2: tag, $i + 3: tail.
470
		for ($i = 0, $numParts = count($parts) - 1; $i < $numParts; $i += 4)
471
		{
472
			$tag = strtolower($parts[$i + 2]);
473
			$isOpeningTag = $parts[$i + 1] === '';
474
475
			if ($isOpeningTag)
476
			{
477
				switch ($tag)
478
				{
479
					case 'ol':
480
					case 'ul':
481
482
						// We have a problem, we're already in a list.
483
						if ($inList)
484
						{
485
							// Inject a list opener, we'll deal with the ol/ul next loop.
486
							array_splice($parts, $i, 0, array(
487
								'',
488
								'',
489
								str_repeat("\t", $listDepth) . '[li]',
490
								'',
491
							));
492
							$numParts = count($parts) - 1;
493
494
							// The inlist status changes a bit.
495
							$inList = false;
496
						}
497
498
						// Just starting a new list.
499
						else
500
						{
501
							$inList = true;
502
503
							if ($tag === 'ol')
504
								$listType = 'decimal';
505
							elseif (preg_match('~type="?(' . implode('|', array_keys($listTypeMapping)) . ')"?~', $parts[$i + 3], $match) === 1)
506
								$listType = $listTypeMapping[$match[1]];
507
							else
508
								$listType = null;
509
510
							$listDepth++;
511
512
							$parts[$i + 2] = '[list' . ($listType === null ? '' : ' type=' . $listType) . ']' . "\n";
513
							$parts[$i + 3] = '';
514
						}
515
						break;
516
517
					case 'li':
518
519
						// This is how it should be: a list item inside the list.
520
						if ($inList)
521
						{
522
							$parts[$i + 2] = str_repeat("\t", $listDepth) . '[li]';
523
							$parts[$i + 3] = '';
524
525
							// Within a list item, it's almost as if you're outside.
526
							$inList = false;
527
						}
528
529
						// The li is no direct child of a list.
530
						else
531
						{
532
							// We are apparently in a list item.
533
							if ($listDepth > 0)
534
							{
535
								$parts[$i + 2] = '[/li]' . "\n" . str_repeat("\t", $listDepth) . '[li]';
536
								$parts[$i + 3] = '';
537
							}
538
539
							// We're not even near a list.
540
							else
541
							{
542
								// Quickly create a list with an item.
543
								$listDepth++;
544
545
								$parts[$i + 2] = '[list]' . "\n\t" . '[li]';
546
								$parts[$i + 3] = '';
547
							}
548
						}
549
550
						break;
551
				}
552
			}
553
554
			// Handle all the closing tags.
555
			else
556
			{
557
				switch ($tag)
558
				{
559
					case 'ol':
560
					case 'ul':
561
562
						// As we expected it, closing the list while we're in it.
563
						if ($inList)
564
						{
565
							$inList = false;
566
567
							$listDepth--;
568
569
							$parts[$i + 1] = '';
570
							$parts[$i + 2] = str_repeat("\t", $listDepth) . '[/list]';
571
							$parts[$i + 3] = '';
572
						}
573
574
						else
575
						{
576
							// We're in a list item.
577
							if ($listDepth > 0)
578
							{
579
								// Inject closure for this list item first.
580
								// The content of $parts[$i] is left as is!
581
								array_splice($parts, $i + 1, 0, array(
582
									'', // $i + 1
583
									'[/li]' . "\n", // $i + 2
584
									'', // $i + 3
585
									'', // $i + 4
586
								));
587
								$numParts = count($parts) - 1;
588
589
								// Now that we've closed the li, we're in list space.
590
								$inList = true;
591
							}
592
593
							// We're not even in a list, ignore
594
							else
595
							{
596
								$parts[$i + 1] = '';
597
								$parts[$i + 2] = '';
598
								$parts[$i + 3] = '';
599
							}
600
						}
601
						break;
602
603
					case 'li':
604
605
						if ($inList)
606
						{
607
							// There's no use for a </li> after <ol> or <ul>, ignore.
608
							$parts[$i + 1] = '';
609
							$parts[$i + 2] = '';
610
							$parts[$i + 3] = '';
611
						}
612
613
						else
614
						{
615
							// Remove the trailing breaks from the list item.
616
							$parts[$i] = preg_replace('~\s*<br\s*' . '/?' . '>\s*$~', '', $parts[$i]);
617
							$parts[$i + 1] = '';
618
							$parts[$i + 2] = '[/li]' . "\n";
619
							$parts[$i + 3] = '';
620
621
							// And we're back in the [list] space.
622
							$inList = true;
623
						}
624
625
						break;
626
				}
627
			}
628
629
			// If we're in the [list] space, no content is allowed.
630
			if ($inList && trim(preg_replace('~\s*<br\s*' . '/?' . '>\s*~', '', $parts[$i + 4])) !== '')
631
			{
632
				// Fix it by injecting an extra list item.
633
				array_splice($parts, $i + 4, 0, array(
634
					'', // No content.
635
					'', // Opening tag.
636
					'li', // It's a <li>.
637
					'', // No tail.
638
				));
639
				$numParts = count($parts) - 1;
640
			}
641
		}
642
643
		$text = implode('', $parts);
644
645
		if ($inList)
646
		{
647
			$listDepth--;
648
			$text .= str_repeat("\t", $listDepth) . '[/list]';
649
		}
650
651
		for ($i = $listDepth; $i > 0; $i--)
652
			$text .= '[/li]' . "\n" . str_repeat("\t", $i - 1) . '[/list]';
653
	}
654
655
	// I love my own image...
656
	while (preg_match('~<img\s+([^<>]*)/*>~i', $text, $matches) === 1)
657
	{
658
		// Find the position of the image.
659
		$start_pos = strpos($text, $matches[0]);
660
		if ($start_pos === false)
661
			break;
662
		$end_pos = $start_pos + strlen($matches[0]);
663
664
		$params = '';
665
		$src = '';
666
667
		$attrs = fetchTagAttributes($matches[1]);
668
		foreach ($attrs as $attrib => $value)
669
		{
670
			if (in_array($attrib, array('width', 'height')))
671
				$params .= ' ' . $attrib . '=' . (int) $value;
672
			elseif ($attrib == 'alt' && trim($value) != '')
673
				$params .= ' alt=' . trim($value);
674
			elseif ($attrib == 'src')
675
				$src = trim($value);
676
		}
677
678
		$tag = '';
679
		if (!empty($src))
680
		{
681
			// Attempt to fix the path in case it's not present.
682
			if (preg_match('~^https?://~i', $src) === 0 && is_array($parsedURL = parse_iri($scripturl)) && isset($parsedURL['host']))
683
			{
684
				$baseURL = (isset($parsedURL['scheme']) ? $parsedURL['scheme'] : 'http') . '://' . $parsedURL['host'] . (empty($parsedURL['port']) ? '' : ':' . $parsedURL['port']);
685
686
				if (substr($src, 0, 1) === '/')
687
					$src = $baseURL . $src;
688
				else
689
					$src = $baseURL . (empty($parsedURL['path']) ? '/' : preg_replace('~/(?:index\\.php)?$~', '', $parsedURL['path'])) . '/' . $src;
690
			}
691
692
			$tag = '[img' . $params . ']' . $src . '[/img]';
693
		}
694
695
		// Replace the tag
696
		$text = substr($text, 0, $start_pos) . $tag . substr($text, $end_pos);
697
	}
698
699
	// The final bits are the easy ones - tags which map to tags which map to tags - etc etc.
700
	$tags = array(
701
		'~<b(\s(.)*?)*?' . '>~i' => function()
702
		{
703
			return '[b]';
704
		},
705
		'~</b>~i' => function()
706
		{
707
			return '[/b]';
708
		},
709
		'~<i(\s(.)*?)*?' . '>~i' => function()
710
		{
711
			return '[i]';
712
		},
713
		'~</i>~i' => function()
714
		{
715
			return '[/i]';
716
		},
717
		'~<u(\s(.)*?)*?' . '>~i' => function()
718
		{
719
			return '[u]';
720
		},
721
		'~</u>~i' => function()
722
		{
723
			return '[/u]';
724
		},
725
		'~<strong(\s(.)*?)*?' . '>~i' => function()
726
		{
727
			return '[b]';
728
		},
729
		'~</strong>~i' => function()
730
		{
731
			return '[/b]';
732
		},
733
		'~<em(\s(.)*?)*?' . '>~i' => function()
734
		{
735
			return '[i]';
736
		},
737
		'~</em>~i' => function()
738
		{
739
			return '[i]';
740
		},
741
		'~<s(\s(.)*?)*?' . '>~i' => function()
742
		{
743
			return "[s]";
744
		},
745
		'~</s>~i' => function()
746
		{
747
			return "[/s]";
748
		},
749
		'~<strike(\s(.)*?)*?' . '>~i' => function()
750
		{
751
			return '[s]';
752
		},
753
		'~</strike>~i' => function()
754
		{
755
			return '[/s]';
756
		},
757
		'~<del(\s(.)*?)*?' . '>~i' => function()
758
		{
759
			return '[s]';
760
		},
761
		'~</del>~i' => function()
762
		{
763
			return '[/s]';
764
		},
765
		'~<center(\s(.)*?)*?' . '>~i' => function()
766
		{
767
			return '[center]';
768
		},
769
		'~</center>~i' => function()
770
		{
771
			return '[/center]';
772
		},
773
		'~<pre(\s(.)*?)*?' . '>~i' => function()
774
		{
775
			return '[pre]';
776
		},
777
		'~</pre>~i' => function()
778
		{
779
			return '[/pre]';
780
		},
781
		'~<sub(\s(.)*?)*?' . '>~i' => function()
782
		{
783
			return '[sub]';
784
		},
785
		'~</sub>~i' => function()
786
		{
787
			return '[/sub]';
788
		},
789
		'~<sup(\s(.)*?)*?' . '>~i' => function()
790
		{
791
			return '[sup]';
792
		},
793
		'~</sup>~i' => function()
794
		{
795
			return '[/sup]';
796
		},
797
		'~<tt(\s(.)*?)*?' . '>~i' => function()
798
		{
799
			return '[tt]';
800
		},
801
		'~</tt>~i' => function()
802
		{
803
			return '[/tt]';
804
		},
805
		'~<table(\s(.)*?)*?' . '>~i' => function()
806
		{
807
			return '[table]';
808
		},
809
		'~</table>~i' => function()
810
		{
811
			return '[/table]';
812
		},
813
		'~<tr(\s(.)*?)*?' . '>~i' => function()
814
		{
815
			return '[tr]';
816
		},
817
		'~</tr>~i' => function()
818
		{
819
			return '[/tr]';
820
		},
821
		'~<(td|th)\s[^<>]*?colspan="?(\d{1,2})"?.*?' . '>~i' => function($matches)
822
		{
823
			return str_repeat('[td][/td]', $matches[2] - 1) . '[td]';
824
		},
825
		'~<(td|th)(\s(.)*?)*?' . '>~i' => function()
826
		{
827
			return '[td]';
828
		},
829
		'~</(td|th)>~i' => function()
830
		{
831
			return '[/td]';
832
		},
833
		'~<br(?:\s[^<>]*?)?' . '>~i' => function()
834
		{
835
			return "\n";
836
		},
837
		'~<hr[^<>]*>(\n)?~i' => function($matches)
838
		{
839
			return "[hr]\n" . $matches[0];
840
		},
841
		'~(\n)?\\[hr\\]~i' => function()
842
		{
843
			return "\n[hr]";
844
		},
845
		'~^\n\\[hr\\]~i' => function()
846
		{
847
			return "[hr]";
848
		},
849
		'~<blockquote(\s(.)*?)*?' . '>~i' => function()
850
		{
851
			return "&lt;blockquote&gt;";
852
		},
853
		'~</blockquote>~i' => function()
854
		{
855
			return "&lt;/blockquote&gt;";
856
		},
857
		'~<ins(\s(.)*?)*?' . '>~i' => function()
858
		{
859
			return "&lt;ins&gt;";
860
		},
861
		'~</ins>~i' => function()
862
		{
863
			return "&lt;/ins&gt;";
864
		},
865
	);
866
867
	foreach ($tags as $tag => $replace)
868
		$text = preg_replace_callback($tag, $replace, $text);
869
870
	// Please give us just a little more time.
871
	if (connection_aborted() && $context['server']['is_apache'])
872
		@apache_reset_timeout();
873
874
	// What about URL's - the pain in the ass of the tag world.
875
	while (preg_match('~<a\s+([^<>]*)>([^<>]*)</a>~i', $text, $matches) === 1)
876
	{
877
		// Find the position of the URL.
878
		$start_pos = strpos($text, $matches[0]);
879
		if ($start_pos === false)
880
			break;
881
		$end_pos = $start_pos + strlen($matches[0]);
882
883
		$tag_type = 'url';
884
		$href = '';
885
886
		$attrs = fetchTagAttributes($matches[1]);
887
		foreach ($attrs as $attrib => $value)
888
		{
889
			if ($attrib == 'href')
890
			{
891
				$href = trim($value);
892
893
				// Are we dealing with an FTP link?
894
				if (preg_match('~^ftps?://~', $href) === 1)
895
					$tag_type = 'ftp';
896
897
				// Or is this a link to an email address?
898
				elseif (substr($href, 0, 7) == 'mailto:')
899
				{
900
					$tag_type = 'email';
901
					$href = substr($href, 7);
902
				}
903
904
				// No http(s), so attempt to fix this potential relative URL.
905
				elseif (preg_match('~^https?://~i', $href) === 0 && is_array($parsedURL = parse_iri($scripturl)) && isset($parsedURL['host']))
906
				{
907
					$baseURL = (isset($parsedURL['scheme']) ? $parsedURL['scheme'] : 'http') . '://' . $parsedURL['host'] . (empty($parsedURL['port']) ? '' : ':' . $parsedURL['port']);
908
909
					if (substr($href, 0, 1) === '/')
910
						$href = $baseURL . $href;
911
					else
912
						$href = $baseURL . (empty($parsedURL['path']) ? '/' : preg_replace('~/(?:index\\.php)?$~', '', $parsedURL['path'])) . '/' . $href;
913
				}
914
			}
915
916
			// External URL?
917
			if ($attrib == 'target' && $tag_type == 'url')
918
			{
919
				if (trim($value) == '_blank')
920
					$tag_type == 'iurl';
921
			}
922
		}
923
924
		$tag = '';
925
		if ($href != '')
926
		{
927
			if ($matches[2] == $href)
928
				$tag = '[' . $tag_type . ']' . $href . '[/' . $tag_type . ']';
929
			else
930
				$tag = '[' . $tag_type . '=' . $href . ']' . $matches[2] . '[/' . $tag_type . ']';
931
		}
932
933
		// Replace the tag
934
		$text = substr($text, 0, $start_pos) . $tag . substr($text, $end_pos);
935
	}
936
937
	$text = strip_tags($text);
938
939
	// Some tags often end up as just dummy tags - remove those.
940
	$text = preg_replace('~\[[bisu]\]\s*\[/[bisu]\]~', '', $text);
941
942
	// Fix up entities.
943
	$text = preg_replace('~&#38;~i', '&#38;#38;', $text);
944
945
	$text = legalise_bbc($text);
946
947
	return $text;
948
}
949
950
/**
951
 * Returns an array of attributes associated with a tag.
952
 *
953
 * @param string $text A tag
954
 * @return array An array of attributes
955
 */
956
function fetchTagAttributes($text)
957
{
958
	$attribs = array();
959
	$key = $value = '';
960
	$tag_state = 0; // 0 = key, 1 = attribute with no string, 2 = attribute with string
961
	for ($i = 0; $i < strlen($text); $i++)
962
	{
963
		// We're either moving from the key to the attribute or we're in a string and this is fine.
964
		if ($text[$i] == '=')
965
		{
966
			if ($tag_state == 0)
967
				$tag_state = 1;
968
			elseif ($tag_state == 2)
969
				$value .= '=';
970
		}
971
		// A space is either moving from an attribute back to a potential key or in a string is fine.
972
		elseif ($text[$i] == ' ')
973
		{
974
			if ($tag_state == 2)
975
				$value .= ' ';
976
			elseif ($tag_state == 1)
977
			{
978
				$attribs[$key] = $value;
979
				$key = $value = '';
980
				$tag_state = 0;
981
			}
982
		}
983
		// A quote?
984
		elseif ($text[$i] == '"')
985
		{
986
			// Must be either going into or out of a string.
987
			if ($tag_state == 1)
988
				$tag_state = 2;
989
			else
990
				$tag_state = 1;
991
		}
992
		// Otherwise it's fine.
993
		else
994
		{
995
			if ($tag_state == 0)
996
				$key .= $text[$i];
997
			else
998
				$value .= $text[$i];
999
		}
1000
	}
1001
1002
	// Anything left?
1003
	if ($key != '' && $value != '')
0 ignored issues
show
The condition $key != '' is always false.
Loading history...
1004
		$attribs[$key] = $value;
1005
1006
	return $attribs;
1007
}
1008
1009
/**
1010
 * Attempt to clean up illegal BBC caused by browsers like Opera which don't obey the rules
1011
 *
1012
 * @param string $text Text
1013
 * @return string Cleaned up text
1014
 */
1015
function legalise_bbc($text)
1016
{
1017
	global $modSettings;
1018
1019
	// Don't care about the texts that are too short.
1020
	if (strlen($text) < 3)
1021
		return $text;
1022
1023
	// A list of tags that's disabled by the admin.
1024
	$disabled = empty($modSettings['disabledBBC']) ? array() : array_flip(explode(',', strtolower($modSettings['disabledBBC'])));
1025
1026
	// Get a list of all the tags that are not disabled.
1027
	$all_tags = parse_bbc(false);
1028
	$valid_tags = array();
1029
	$self_closing_tags = array();
1030
	foreach ($all_tags as $tag)
0 ignored issues
show
The expression $all_tags of type string is not traversable.
Loading history...
1031
	{
1032
		if (!isset($disabled[$tag['tag']]))
1033
			$valid_tags[$tag['tag']] = !empty($tag['block_level']);
1034
		if (isset($tag['type']) && $tag['type'] == 'closed')
1035
			$self_closing_tags[] = $tag['tag'];
1036
	}
1037
1038
	// 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!
1039
	$align_tags = array('left', 'center', 'right', 'pre');
1040
1041
	// Remove those align tags that are not valid.
1042
	$align_tags = array_intersect($align_tags, array_keys($valid_tags));
1043
1044
	// These keep track of where we are!
1045
	if (!empty($align_tags) && count($matches = preg_split('~(\\[/?(?:' . implode('|', $align_tags) . ')\\])~', $text, -1, PREG_SPLIT_DELIM_CAPTURE)) > 1)
1046
	{
1047
		// The first one is never a tag.
1048
		$isTag = false;
1049
1050
		// By default we're not inside a tag too.
1051
		$insideTag = null;
1052
1053
		foreach ($matches as $i => $match)
1054
		{
1055
			// We're only interested in tags, not text.
1056
			if ($isTag)
1057
			{
1058
				$isClosingTag = substr($match, 1, 1) === '/';
1059
				$tagName = substr($match, $isClosingTag ? 2 : 1, -1);
1060
1061
				// We're closing the exact same tag that we opened.
1062
				if ($isClosingTag && $insideTag === $tagName)
1063
					$insideTag = null;
1064
1065
				// We're opening a tag and we're not yet inside one either
1066
				elseif (!$isClosingTag && $insideTag === null)
1067
					$insideTag = $tagName;
1068
1069
				// In all other cases, this tag must be invalid
1070
				else
1071
					unset($matches[$i]);
1072
			}
1073
1074
			// The next one is gonna be the other one.
1075
			$isTag = !$isTag;
0 ignored issues
show
$isTag is of type mixed, thus it always evaluated to false.
Loading history...
1076
		}
1077
1078
		// We're still inside a tag and had no chance for closure?
1079
		if ($insideTag !== null)
0 ignored issues
show
The condition $insideTag !== null is always false.
Loading history...
1080
			$matches[] = '[/' . $insideTag . ']';
1081
1082
		// And a complete text string again.
1083
		$text = implode('', $matches);
1084
	}
1085
1086
	// Quickly remove any tags which are back to back.
1087
	$backToBackPattern = '~\\[(' . implode('|', array_diff(array_keys($valid_tags), array('td', 'anchor'))) . ')[^<>\\[\\]]*\\]\s*\\[/\\1\\]~';
1088
	$lastlen = 0;
1089
	while (strlen($text) !== $lastlen)
1090
		$lastlen = strlen($text = preg_replace($backToBackPattern, '', $text));
1091
1092
	// Need to sort the tags by name length.
1093
	uksort(
1094
		$valid_tags,
1095
		function($a, $b)
1096
		{
1097
			return strlen($a) < strlen($b) ? 1 : -1;
1098
		}
1099
	);
1100
1101
	// These inline tags can compete with each other regarding style.
1102
	$competing_tags = array(
1103
		'color',
1104
		'size',
1105
	);
1106
1107
	// These keep track of where we are!
1108
	if (count($parts = preg_split(sprintf('~(\\[)(/?)(%1$s)((?:[\\s=][^\\]\\[]*)?\\])~', implode('|', array_keys($valid_tags))), $text, -1, PREG_SPLIT_DELIM_CAPTURE)) > 1)
1109
	{
1110
		// Start outside [nobbc] or [code] blocks.
1111
		$inCode = false;
1112
		$inNoBbc = false;
1113
1114
		// A buffer containing all opened inline elements.
1115
		$inlineElements = array();
1116
1117
		// A buffer containing all opened block elements.
1118
		$blockElements = array();
1119
1120
		// A buffer containing the opened inline elements that might compete.
1121
		$competingElements = array();
1122
1123
		// $i: text, $i + 1: '[', $i + 2: '/', $i + 3: tag, $i + 4: tag tail.
1124
		for ($i = 0, $n = count($parts) - 1; $i < $n; $i += 5)
1125
		{
1126
			$tag = $parts[$i + 3];
1127
			$isOpeningTag = $parts[$i + 2] === '';
1128
			$isClosingTag = $parts[$i + 2] === '/';
1129
			$isBlockLevelTag = isset($valid_tags[$tag]) && $valid_tags[$tag] && !in_array($tag, $self_closing_tags);
1130
			$isCompetingTag = in_array($tag, $competing_tags);
1131
1132
			// Check if this might be one of those cleaned out tags.
1133
			if ($tag === '')
1134
				continue;
1135
1136
			// Special case: inside [code] blocks any code is left untouched.
1137
			elseif ($tag === 'code')
1138
			{
1139
				// We're inside a code block and closing it.
1140
				if ($inCode && $isClosingTag)
1141
				{
1142
					$inCode = false;
1143
1144
					// Reopen tags that were closed before the code block.
1145
					if (!empty($inlineElements))
1146
						$parts[$i + 4] .= '[' . implode('][', array_keys($inlineElements)) . ']';
1147
				}
1148
1149
				// We're outside a coding and nobbc block and opening it.
1150
				elseif (!$inCode && !$inNoBbc && $isOpeningTag)
1151
				{
1152
					// If there are still inline elements left open, close them now.
1153
					if (!empty($inlineElements))
1154
					{
1155
						$parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']';
1156
						//$inlineElements = array();
1157
					}
1158
1159
					$inCode = true;
1160
				}
1161
1162
				// Nothing further to do.
1163
				continue;
1164
			}
1165
1166
			// Special case: inside [nobbc] blocks any BBC is left untouched.
1167
			elseif ($tag === 'nobbc')
1168
			{
1169
				// We're inside a nobbc block and closing it.
1170
				if ($inNoBbc && $isClosingTag)
1171
				{
1172
					$inNoBbc = false;
1173
1174
					// Some inline elements might've been closed that need reopening.
1175
					if (!empty($inlineElements))
1176
						$parts[$i + 4] .= '[' . implode('][', array_keys($inlineElements)) . ']';
1177
				}
1178
1179
				// We're outside a nobbc and coding block and opening it.
1180
				elseif (!$inNoBbc && !$inCode && $isOpeningTag)
1181
				{
1182
					// Can't have inline elements still opened.
1183
					if (!empty($inlineElements))
1184
					{
1185
						$parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']';
1186
						//$inlineElements = array();
1187
					}
1188
1189
					$inNoBbc = true;
1190
				}
1191
1192
				continue;
1193
			}
1194
1195
			// So, we're inside one of the special blocks: ignore any tag.
1196
			elseif ($inCode || $inNoBbc)
1197
				continue;
1198
1199
			// We're dealing with an opening tag.
1200
			if ($isOpeningTag)
1201
			{
1202
				// Everyting inside the square brackets of the opening tag.
1203
				$elementContent = $parts[$i + 3] . substr($parts[$i + 4], 0, -1);
1204
1205
				// A block level opening tag.
1206
				if ($isBlockLevelTag)
1207
				{
1208
					// Are there inline elements still open?
1209
					if (!empty($inlineElements))
1210
					{
1211
						// Close all the inline tags, a block tag is coming...
1212
						$parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']';
1213
1214
						// Now open them again, we're inside the block tag now.
1215
						$parts[$i + 5] = '[' . implode('][', array_keys($inlineElements)) . ']' . $parts[$i + 5];
1216
					}
1217
1218
					$blockElements[] = $tag;
1219
				}
1220
1221
				// Inline opening tag.
1222
				elseif (!in_array($tag, $self_closing_tags))
1223
				{
1224
					// Can't have two opening elements with the same contents!
1225
					if (isset($inlineElements[$elementContent]))
1226
					{
1227
						// Get rid of this tag.
1228
						$parts[$i + 1] = $parts[$i + 2] = $parts[$i + 3] = $parts[$i + 4] = '';
1229
1230
						// Now try to find the corresponding closing tag.
1231
						$curLevel = 1;
1232
						for ($j = $i + 5, $m = count($parts) - 1; $j < $m; $j += 5)
1233
						{
1234
							// Find the tags with the same tagname
1235
							if ($parts[$j + 3] === $tag)
1236
							{
1237
								// If it's an opening tag, increase the level.
1238
								if ($parts[$j + 2] === '')
1239
									$curLevel++;
1240
1241
								// A closing tag, decrease the level.
1242
								else
1243
								{
1244
									$curLevel--;
1245
1246
									// Gotcha! Clean out this closing tag gone rogue.
1247
									if ($curLevel === 0)
1248
									{
1249
										$parts[$j + 1] = $parts[$j + 2] = $parts[$j + 3] = $parts[$j + 4] = '';
1250
										break;
1251
									}
1252
								}
1253
							}
1254
						}
1255
					}
1256
1257
					// Otherwise, add this one to the list.
1258
					else
1259
					{
1260
						if ($isCompetingTag)
1261
						{
1262
							if (!isset($competingElements[$tag]))
1263
								$competingElements[$tag] = array();
1264
1265
							$competingElements[$tag][] = $parts[$i + 4];
1266
1267
							if (count($competingElements[$tag]) > 1)
1268
								$parts[$i] .= '[/' . $tag . ']';
1269
						}
1270
1271
						$inlineElements[$elementContent] = $tag;
1272
					}
1273
				}
1274
			}
1275
1276
			// Closing tag.
1277
			else
1278
			{
1279
				// Closing the block tag.
1280
				if ($isBlockLevelTag)
1281
				{
1282
					// Close the elements that should've been closed by closing this tag.
1283
					if (!empty($blockElements))
1284
					{
1285
						$addClosingTags = array();
1286
						while ($element = array_pop($blockElements))
1287
						{
1288
							if ($element === $tag)
1289
								break;
1290
1291
							// Still a block tag was open not equal to this tag.
1292
							$addClosingTags[] = $element['type'];
1293
						}
1294
1295
						if (!empty($addClosingTags))
1296
							$parts[$i + 1] = '[/' . implode('][/', array_reverse($addClosingTags)) . ']' . $parts[$i + 1];
1297
1298
						// Apparently the closing tag was not found on the stack.
1299
						if (!is_string($element) || $element !== $tag)
1300
						{
1301
							// Get rid of this particular closing tag, it was never opened.
1302
							$parts[$i + 1] = substr($parts[$i + 1], 0, -1);
1303
							$parts[$i + 2] = $parts[$i + 3] = $parts[$i + 4] = '';
1304
							continue;
1305
						}
1306
					}
1307
					else
1308
					{
1309
						// Get rid of this closing tag!
1310
						$parts[$i + 1] = $parts[$i + 2] = $parts[$i + 3] = $parts[$i + 4] = '';
1311
						continue;
1312
					}
1313
1314
					// Inline elements are still left opened?
1315
					if (!empty($inlineElements))
1316
					{
1317
						// Close them first..
1318
						$parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']';
1319
1320
						// Then reopen them.
1321
						$parts[$i + 5] = '[' . implode('][', array_keys($inlineElements)) . ']' . $parts[$i + 5];
1322
					}
1323
				}
1324
				// Inline tag.
1325
				else
1326
				{
1327
					// Are we expecting this tag to end?
1328
					if (in_array($tag, $inlineElements))
1329
					{
1330
						foreach (array_reverse($inlineElements, true) as $tagContentToBeClosed => $tagToBeClosed)
1331
						{
1332
							// Closing it one way or the other.
1333
							unset($inlineElements[$tagContentToBeClosed]);
1334
1335
							// Was this the tag we were looking for?
1336
							if ($tagToBeClosed === $tag)
1337
								break;
1338
1339
							// Nope, close it and look further!
1340
							else
1341
								$parts[$i] .= '[/' . $tagToBeClosed . ']';
1342
						}
1343
1344
						if ($isCompetingTag && !empty($competingElements[$tag]))
1345
						{
1346
							array_pop($competingElements[$tag]);
1347
1348
							if (count($competingElements[$tag]) > 0)
1349
								$parts[$i + 5] = '[' . $tag . $competingElements[$tag][count($competingElements[$tag]) - 1] . $parts[$i + 5];
1350
						}
1351
					}
1352
1353
					// Unexpected closing tag, ex-ter-mi-nate.
1354
					else
1355
						$parts[$i + 1] = $parts[$i + 2] = $parts[$i + 3] = $parts[$i + 4] = '';
1356
				}
1357
			}
1358
		}
1359
1360
		// Close the code tags.
1361
		if ($inCode)
0 ignored issues
show
The condition $inCode is always false.
Loading history...
1362
			$parts[$i] .= '[/code]';
1363
1364
		// The same for nobbc tags.
1365
		elseif ($inNoBbc)
0 ignored issues
show
The condition $inNoBbc is always false.
Loading history...
1366
			$parts[$i] .= '[/nobbc]';
1367
1368
		// Still inline tags left unclosed? Close them now, better late than never.
1369
		elseif (!empty($inlineElements))
1370
			$parts[$i] .= '[/' . implode('][/', array_reverse($inlineElements)) . ']';
1371
1372
		// Now close the block elements.
1373
		if (!empty($blockElements))
1374
			$parts[$i] .= '[/' . implode('][/', array_reverse($blockElements)) . ']';
1375
1376
		$text = implode('', $parts);
1377
	}
1378
1379
	// Final clean up of back to back tags.
1380
	$lastlen = 0;
1381
	while (strlen($text) !== $lastlen)
1382
		$lastlen = strlen($text = preg_replace($backToBackPattern, '', $text));
1383
1384
	return $text;
1385
}
1386
1387
/**
1388
 * Retrieves a list of message icons.
1389
 * - Based on the settings, the array will either contain a list of default
1390
 *   message icons or a list of custom message icons retrieved from the database.
1391
 * - The board_id is needed for the custom message icons (which can be set for
1392
 *   each board individually).
1393
 *
1394
 * @param int $board_id The ID of the board
1395
 * @return array An array of info about available icons
1396
 */
1397
function getMessageIcons($board_id)
1398
{
1399
	global $modSettings, $txt, $settings, $smcFunc;
1400
1401
	if (empty($modSettings['messageIcons_enable']))
1402
	{
1403
		loadLanguage('Post');
1404
1405
		$icons = array(
1406
			array('value' => 'xx', 'name' => $txt['standard']),
1407
			array('value' => 'thumbup', 'name' => $txt['thumbs_up']),
1408
			array('value' => 'thumbdown', 'name' => $txt['thumbs_down']),
1409
			array('value' => 'exclamation', 'name' => $txt['exclamation_point']),
1410
			array('value' => 'question', 'name' => $txt['question_mark']),
1411
			array('value' => 'lamp', 'name' => $txt['lamp']),
1412
			array('value' => 'smiley', 'name' => $txt['icon_smiley']),
1413
			array('value' => 'angry', 'name' => $txt['icon_angry']),
1414
			array('value' => 'cheesy', 'name' => $txt['icon_cheesy']),
1415
			array('value' => 'grin', 'name' => $txt['icon_grin']),
1416
			array('value' => 'sad', 'name' => $txt['icon_sad']),
1417
			array('value' => 'wink', 'name' => $txt['icon_wink']),
1418
			array('value' => 'poll', 'name' => $txt['icon_poll']),
1419
		);
1420
1421
		foreach ($icons as $k => $dummy)
1422
		{
1423
			$icons[$k]['url'] = $settings['images_url'] . '/post/' . $dummy['value'] . '.png';
1424
			$icons[$k]['is_last'] = false;
1425
		}
1426
	}
1427
	// Otherwise load the icons, and check we give the right image too...
1428
	else
1429
	{
1430
		if (($temp = cache_get_data('posting_icons-' . $board_id, 480)) == null)
1431
		{
1432
			$request = $smcFunc['db_query']('', '
1433
				SELECT title, filename
1434
				FROM {db_prefix}message_icons
1435
				WHERE id_board IN (0, {int:board_id})
1436
				ORDER BY icon_order',
1437
				array(
1438
					'board_id' => $board_id,
1439
				)
1440
			);
1441
			$icon_data = array();
1442
			while ($row = $smcFunc['db_fetch_assoc']($request))
1443
				$icon_data[] = $row;
1444
			$smcFunc['db_free_result']($request);
1445
1446
			$icons = array();
1447
			foreach ($icon_data as $icon)
1448
			{
1449
				$icons[$icon['filename']] = array(
1450
					'value' => $icon['filename'],
1451
					'name' => $icon['title'],
1452
					'url' => $settings[file_exists($settings['theme_dir'] . '/images/post/' . $icon['filename'] . '.png') ? 'images_url' : 'default_images_url'] . '/post/' . $icon['filename'] . '.png',
1453
					'is_last' => false,
1454
				);
1455
			}
1456
1457
			cache_put_data('posting_icons-' . $board_id, $icons, 480);
1458
		}
1459
		else
1460
			$icons = $temp;
1461
	}
1462
	call_integration_hook('integrate_load_message_icons', array(&$icons));
1463
1464
	return array_values($icons);
1465
}
1466
1467
/**
1468
 * Creates a box that can be used for richedit stuff like BBC, Smileys etc.
1469
 *
1470
 * @param array $editorOptions Various options for the editor
1471
 */
1472
function create_control_richedit($editorOptions)
1473
{
1474
	global $txt, $modSettings, $options, $smcFunc, $editortxt;
1475
	global $context, $settings, $user_info, $scripturl;
1476
1477
	// Load the Post language file... for the moment at least.
1478
	loadLanguage('Post');
1479
	loadLanguage('Editor');
1480
	loadLanguage('Drafts');
1481
1482
	$context['richedit_buttons'] = array(
1483
		'save_draft' => array(
1484
			'type' => 'submit',
1485
			'value' => $txt['draft_save'],
1486
			'onclick' => !empty($context['drafts_pm_save']) ? 'submitThisOnce(this);' : (!empty($context['drafts_save']) ? 'return confirm(' . JavaScriptEscape($txt['draft_save_note']) . ') && submitThisOnce(this);' : ''),
1487
			'accessKey' => 'd',
1488
			'show' => !empty($context['drafts_pm_save']) || !empty($context['drafts_save'])
1489
		),
1490
		'id_pm_draft' => array(
1491
			'type' => 'hidden',
1492
			'value' => empty($context['id_pm_draft']) ? 0 : $context['id_pm_draft'],
1493
			'show' => !empty($context['drafts_pm_save'])
1494
		),
1495
		'id_draft' => array(
1496
			'type' => 'hidden',
1497
			'value' => empty($context['id_draft']) ? 0 : $context['id_draft'],
1498
			'show' => !empty($context['drafts_save'])
1499
		),
1500
		'spell_check' => array(
1501
			'type' => 'submit',
1502
			'value' => $txt['spell_check'],
1503
			'show' => !empty($context['show_spellchecking'])
1504
		),
1505
		'preview' => array(
1506
			'type' => 'submit',
1507
			'value' => $txt['preview'],
1508
			'accessKey' => 'p'
1509
		)
1510
	);
1511
1512
	// Every control must have a ID!
1513
	assert(isset($editorOptions['id']));
1514
	assert(isset($editorOptions['value']));
1515
1516
	// Is this the first richedit - if so we need to ensure some template stuff is initialised.
1517
	if (empty($context['controls']['richedit']))
1518
	{
1519
		// Some general stuff.
1520
		$settings['smileys_url'] = $modSettings['smileys_url'] . '/' . $user_info['smiley_set'];
1521
		if (!empty($context['drafts_autosave']))
1522
			$context['drafts_autosave_frequency'] = empty($modSettings['drafts_autosave_frequency']) ? 60000 : $modSettings['drafts_autosave_frequency'] * 1000;
1523
1524
		// This really has some WYSIWYG stuff.
1525
		loadCSSFile('jquery.sceditor.css', array('default_theme' => true, 'validate' => true), 'smf_jquery_sceditor');
1526
		loadTemplate('GenericControls');
1527
1528
		/*
1529
		 *		THEME AUTHORS:
1530
		 			If you want to change or tweak the CSS for the editor,
1531
					include a file named 'jquery.sceditor.theme.css' in your theme.
1532
		*/
1533
		loadCSSFile('jquery.sceditor.theme.css', array('force_current' => true, 'validate' => true,), 'smf_jquery_sceditor_theme');
1534
1535
		// JS makes the editor go round
1536
		loadJavaScriptFile('editor.js', array('minimize' => true), 'smf_editor');
1537
		loadJavaScriptFile('jquery.sceditor.bbcode.min.js', array(), 'smf_sceditor_bbcode');
1538
		loadJavaScriptFile('jquery.sceditor.smf.js', array('minimize' => true), 'smf_sceditor_smf');
1539
1540
		$scExtraLangs = '
1541
		$.sceditor.locale["' . $txt['lang_dictionary'] . '"] = {
1542
			"Width (optional):": "' . $editortxt['width'] . '",
1543
			"Height (optional):": "' . $editortxt['height'] . '",
1544
			"Insert": "' . $editortxt['insert'] . '",
1545
			"Description (optional):": "' . $editortxt['description'] . '",
1546
			"Rows:": "' . $editortxt['rows'] . '",
1547
			"Cols:": "' . $editortxt['cols'] . '",
1548
			"URL:": "' . $editortxt['url'] . '",
1549
			"E-mail:": "' . $editortxt['email'] . '",
1550
			"Video URL:": "' . $editortxt['video_url'] . '",
1551
			"More": "' . $editortxt['more'] . '",
1552
			"Close": "' . $editortxt['close'] . '",
1553
			dateFormat: "' . $editortxt['dateformat'] . '"
1554
		};';
1555
1556
		addInlineJavaScript($scExtraLangs, true);
1557
1558
		addInlineJavaScript('
1559
		var smf_smileys_url = \'' . $settings['smileys_url'] . '\';
1560
		var bbc_quote_from = \'' . addcslashes($txt['quote_from'], "'") . '\';
1561
		var bbc_quote = \'' . addcslashes($txt['quote'], "'") . '\';
1562
		var bbc_search_on = \'' . addcslashes($txt['search_on'], "'") . '\';');
1563
1564
		$context['shortcuts_text'] = $txt['shortcuts' . (!empty($context['drafts_save']) ? '_drafts' : '') . (stripos($_SERVER['HTTP_USER_AGENT'], 'Macintosh') !== false ? '_mac' : (isBrowser('is_firefox') ? '_firefox' : ''))];
1565
1566
		if ($context['show_spellchecking'])
1567
		{
1568
			loadJavaScriptFile('spellcheck.js', array('minimize' => true), 'smf_spellcheck');
1569
1570
			// Some hidden information is needed in order to make the spell checking work.
1571
			if (!isset($_REQUEST['xml']))
1572
				$context['insert_after_template'] .= '
1573
		<form name="spell_form" id="spell_form" method="post" accept-charset="' . $context['character_set'] . '" target="spellWindow" action="' . $scripturl . '?action=spellcheck">
1574
			<input type="hidden" name="spellstring" value="">
1575
		</form>';
1576
		}
1577
	}
1578
1579
	// The [#] item code for creating list items causes issues with SCEditor, but [+] is a safe equivalent.
1580
	$editorOptions['value'] = str_replace('[#]', '[+]', $editorOptions['value']);
1581
	// Tabs are not shown in the SCEditor, replace with spaces.
1582
	$editorOptions['value'] = str_replace("\t", '    ', $editorOptions['value']);
1583
1584
	// Start off the editor...
1585
	$context['controls']['richedit'][$editorOptions['id']] = array(
1586
		'id' => $editorOptions['id'],
1587
		'value' => $editorOptions['value'],
1588
		'rich_value' => $editorOptions['value'], // 2.0 editor compatibility
1589
		'rich_active' => empty($modSettings['disable_wysiwyg']) && (!empty($options['wysiwyg_default']) || !empty($editorOptions['force_rich']) || !empty($_REQUEST[$editorOptions['id'] . '_mode'])),
1590
		'disable_smiley_box' => !empty($editorOptions['disable_smiley_box']),
1591
		'columns' => isset($editorOptions['columns']) ? $editorOptions['columns'] : 60,
1592
		'rows' => isset($editorOptions['rows']) ? $editorOptions['rows'] : 18,
1593
		'width' => isset($editorOptions['width']) ? $editorOptions['width'] : '70%',
1594
		'height' => isset($editorOptions['height']) ? $editorOptions['height'] : '175px',
1595
		'form' => isset($editorOptions['form']) ? $editorOptions['form'] : 'postmodify',
1596
		'bbc_level' => !empty($editorOptions['bbc_level']) ? $editorOptions['bbc_level'] : 'full',
1597
		'preview_type' => isset($editorOptions['preview_type']) ? (int) $editorOptions['preview_type'] : 1,
1598
		'labels' => !empty($editorOptions['labels']) ? $editorOptions['labels'] : array(),
1599
		'locale' => !empty($txt['lang_dictionary']) && $txt['lang_dictionary'] != 'en' ? $txt['lang_dictionary'] : '',
1600
		'required' => !empty($editorOptions['required']),
1601
	);
1602
1603
	if (empty($context['bbc_tags']))
1604
	{
1605
		// 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!
1606
		// Note: 'before' and 'after' are deprecated as of SMF 2.1. Instead, use a separate JS file to configure the functionality of your toolbar buttons.
1607
		/*
1608
			array(
1609
				'code' => 'b', // Required
1610
				'description' => $editortxt['bold'], // Required
1611
				'image' => 'bold', // Optional
1612
				'before' => '[b]', // Deprecated
1613
				'after' => '[/b]', // Deprecated
1614
			),
1615
		*/
1616
		$context['bbc_tags'] = array();
1617
		$context['bbc_tags'][] = array(
1618
			array(
1619
				'code' => 'bold',
1620
				'description' => $editortxt['bold'],
1621
			),
1622
			array(
1623
				'code' => 'italic',
1624
				'description' => $editortxt['italic'],
1625
			),
1626
			array(
1627
				'code' => 'underline',
1628
				'description' => $editortxt['underline']
1629
			),
1630
			array(
1631
				'code' => 'strike',
1632
				'description' => $editortxt['strikethrough']
1633
			),
1634
			array(
1635
				'code' => 'superscript',
1636
				'description' => $editortxt['superscript']
1637
			),
1638
			array(
1639
				'code' => 'subscript',
1640
				'description' => $editortxt['subscript']
1641
			),
1642
			array(),
1643
			array(
1644
				'code' => 'pre',
1645
				'description' => $editortxt['preformatted_text']
1646
			),
1647
			array(
1648
				'code' => 'left',
1649
				'description' => $editortxt['align_left']
1650
			),
1651
			array(
1652
				'code' => 'center',
1653
				'description' => $editortxt['center']
1654
			),
1655
			array(
1656
				'code' => 'right',
1657
				'description' => $editortxt['align_right']
1658
			),
1659
			array(
1660
				'code' => 'justify',
1661
				'description' => $editortxt['justify']
1662
			),
1663
			array(),
1664
			array(
1665
				'code' => 'font',
1666
				'description' => $editortxt['font_name']
1667
			),
1668
			array(
1669
				'code' => 'size',
1670
				'description' => $editortxt['font_size']
1671
			),
1672
			array(
1673
				'code' => 'color',
1674
				'description' => $editortxt['font_color']
1675
			),
1676
		);
1677
		if (empty($modSettings['disable_wysiwyg']))
1678
		{
1679
			$context['bbc_tags'][count($context['bbc_tags']) - 1][] = array(
1680
				'code' => 'removeformat',
1681
				'description' => $editortxt['remove_formatting'],
1682
			);
1683
		}
1684
		$context['bbc_tags'][] = array(
1685
			array(
1686
				'code' => 'floatleft',
1687
				'description' => $editortxt['float_left']
1688
			),
1689
			array(
1690
				'code' => 'floatright',
1691
				'description' => $editortxt['float_right']
1692
			),
1693
			array(),
1694
			array(
1695
				'code' => 'youtube',
1696
				'description' => $editortxt['insert_youtube_video']
1697
			),
1698
			array(
1699
				'code' => 'image',
1700
				'description' => $editortxt['insert_image']
1701
			),
1702
			array(
1703
				'code' => 'link',
1704
				'description' => $editortxt['insert_link']
1705
			),
1706
			array(
1707
				'code' => 'email',
1708
				'description' => $editortxt['insert_email']
1709
			),
1710
			array(),
1711
			array(
1712
				'code' => 'table',
1713
				'description' => $editortxt['insert_table']
1714
			),
1715
			array(
1716
				'code' => 'code',
1717
				'description' => $editortxt['code']
1718
			),
1719
			array(
1720
				'code' => 'quote',
1721
				'description' => $editortxt['insert_quote']
1722
			),
1723
			array(),
1724
			array(
1725
				'code' => 'bulletlist',
1726
				'description' => $editortxt['bullet_list']
1727
			),
1728
			array(
1729
				'code' => 'orderedlist',
1730
				'description' => $editortxt['numbered_list']
1731
			),
1732
			array(
1733
				'code' => 'horizontalrule',
1734
				'description' => $editortxt['insert_horizontal_rule']
1735
			),
1736
			array(),
1737
			array(
1738
				'code' => 'maximize',
1739
				'description' => $editortxt['maximize']
1740
			),
1741
		);
1742
		if (empty($modSettings['disable_wysiwyg']))
1743
		{
1744
			$context['bbc_tags'][count($context['bbc_tags']) - 1][] = array(
1745
				'code' => 'source',
1746
				'description' => $editortxt['view_source'],
1747
			);
1748
		}
1749
1750
		$editor_tag_map = array(
1751
			'b' => 'bold',
1752
			'i' => 'italic',
1753
			'u' => 'underline',
1754
			's' => 'strike',
1755
			'img' => 'image',
1756
			'url' => 'link',
1757
			'sup' => 'superscript',
1758
			'sub' => 'subscript',
1759
			'hr' => 'horizontalrule',
1760
		);
1761
1762
		// Define this here so mods can add to it via the hook.
1763
		$context['disabled_tags'] = array();
1764
1765
		// Allow mods to modify BBC buttons.
1766
		// Note: passing the array here is not necessary and is deprecated, but it is kept for backward compatibility with 2.0
1767
		call_integration_hook('integrate_bbc_buttons', array(&$context['bbc_tags'], &$editor_tag_map));
1768
1769
		// Generate a list of buttons that shouldn't be shown - this should be the fastest way to do this.
1770
		$disabled_tags = array();
1771
		if (!empty($modSettings['disabledBBC']))
1772
			$disabled_tags = explode(',', $modSettings['disabledBBC']);
1773
1774
		foreach ($disabled_tags as $tag)
1775
		{
1776
			$tag = trim($tag);
1777
1778
			if ($tag === 'list')
1779
			{
1780
				$context['disabled_tags']['bulletlist'] = true;
1781
				$context['disabled_tags']['orderedlist'] = true;
1782
			}
1783
1784
			if ($tag === 'float')
1785
			{
1786
				$context['disabled_tags']['floatleft'] = true;
1787
				$context['disabled_tags']['floatright'] = true;
1788
			}
1789
1790
			foreach ($editor_tag_map as $thisTag => $tagNameBBC)
1791
				if ($tag === $thisTag)
1792
					$context['disabled_tags'][$tagNameBBC] = true;
1793
1794
			$context['disabled_tags'][$tag] = true;
1795
		}
1796
1797
		$bbcodes_styles = '';
1798
		$context['bbcodes_handlers'] = '';
1799
		$context['bbc_toolbar'] = array();
1800
1801
		foreach ($context['bbc_tags'] as $row => $tagRow)
1802
		{
1803
			if (!isset($context['bbc_toolbar'][$row]))
1804
				$context['bbc_toolbar'][$row] = array();
1805
1806
			$tagsRow = array();
1807
1808
			foreach ($tagRow as $tag)
1809
			{
1810
				if (empty($tag['code']))
1811
				{
1812
					$context['bbc_toolbar'][$row][] = implode(',', $tagsRow);
1813
					$tagsRow = array();
1814
				}
1815
				elseif (empty($context['disabled_tags'][$tag['code']]))
1816
				{
1817
					$tagsRow[] = $tag['code'];
1818
1819
					// If we have a custom button image, set it now.
1820
					if (isset($tag['image']))
1821
					{
1822
						$bbcodes_styles .= '
1823
						.sceditor-button-' . $tag['code'] . ' div {
1824
							background: url(\'' . $settings['default_theme_url'] . '/images/bbc/' . $tag['image'] . '.png\');
1825
						}';
1826
					}
1827
1828
					// Set the tooltip and possibly the command info
1829
					$context['bbcodes_handlers'] .= '
1830
						sceditor.command.set(' . JavaScriptEscape($tag['code']) . ', {
1831
							tooltip: ' . JavaScriptEscape(isset($tag['description']) ? $tag['description'] : $tag['code']);
1832
1833
					// Legacy support for 2.0 BBC mods
1834
					if (isset($tag['before']))
1835
					{
1836
						$context['bbcodes_handlers'] .= ',
1837
							exec: function () {
1838
								this.insertText(' . JavaScriptEscape($tag['before']) . (isset($tag['after']) ? ', ' . JavaScriptEscape($tag['after']) : '') . ');
1839
							},
1840
							txtExec: [' . JavaScriptEscape($tag['before']) . (isset($tag['after']) ? ', ' . JavaScriptEscape($tag['after']) : '') . ']';
1841
					}
1842
1843
					$context['bbcodes_handlers'] .= '
1844
						});';
1845
				}
1846
			}
1847
1848
			if (!empty($tagsRow))
1849
				$context['bbc_toolbar'][$row][] = implode(',', $tagsRow);
1850
		}
1851
1852
		if (!empty($bbcodes_styles))
1853
			addInlineCss($bbcodes_styles);
1854
	}
1855
1856
	// Initialize smiley array... if not loaded before.
1857
	if (empty($context['smileys']) && empty($editorOptions['disable_smiley_box']))
1858
	{
1859
		$context['smileys'] = array(
1860
			'postform' => array(),
1861
			'popup' => array(),
1862
		);
1863
1864
		if ($user_info['smiley_set'] != 'none')
1865
		{
1866
			// Cache for longer when customized smiley codes aren't enabled
1867
			$cache_time = empty($modSettings['smiley_enable']) ? 7200 : 480;
1868
1869
			if (($temp = cache_get_data('posting_smileys_' . $user_info['smiley_set'], $cache_time)) == null)
1870
			{
1871
				$request = $smcFunc['db_query']('', '
1872
					SELECT s.code, f.filename, s.description, s.smiley_row, s.hidden
1873
					FROM {db_prefix}smileys AS s
1874
						JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
1875
					WHERE s.hidden IN (0, 2)
1876
						AND f.smiley_set = {string:smiley_set}' . (empty($modSettings['smiley_enable']) ? '
1877
						AND s.code IN ({array_string:default_codes})' : '') . '
1878
					ORDER BY s.smiley_row, s.smiley_order',
1879
					array(
1880
						'default_codes' => array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'),
1881
						'smiley_set' => $user_info['smiley_set'],
1882
					)
1883
				);
1884
				while ($row = $smcFunc['db_fetch_assoc']($request))
1885
				{
1886
					$row['description'] = !empty($txt['icon_' . strtolower($row['description'])]) ? $smcFunc['htmlspecialchars']($txt['icon_' . strtolower($row['description'])]) : $smcFunc['htmlspecialchars']($row['description']);
1887
1888
					$context['smileys'][empty($row['hidden']) ? 'postform' : 'popup'][$row['smiley_row']]['smileys'][] = $row;
1889
				}
1890
				$smcFunc['db_free_result']($request);
1891
1892
				foreach ($context['smileys'] as $section => $smileyRows)
1893
				{
1894
					foreach ($smileyRows as $rowIndex => $smileys)
1895
						$context['smileys'][$section][$rowIndex]['smileys'][count($smileys['smileys']) - 1]['isLast'] = true;
1896
1897
					if (!empty($smileyRows))
1898
						$context['smileys'][$section][count($smileyRows) - 1]['isLast'] = true;
1899
				}
1900
1901
				cache_put_data('posting_smileys_' . $user_info['smiley_set'], $context['smileys'], $cache_time);
1902
			}
1903
			else
1904
				$context['smileys'] = $temp;
1905
		}
1906
	}
1907
1908
	// Set up the SCEditor options
1909
	$sce_options = array(
1910
		'width' => isset($editorOptions['width']) ? $editorOptions['width'] : '100%',
1911
		'height' => isset($editorOptions['height']) ? $editorOptions['height'] : '175px',
1912
		'style' => $settings[file_exists($settings['theme_dir'] . '/css/jquery.sceditor.default.css') ? 'theme_url' : 'default_theme_url'] . '/css/jquery.sceditor.default.css' . $context['browser_cache'],
1913
		'emoticonsCompat' => true,
1914
		'colors' => 'black,maroon,brown,green,navy,grey,red,orange,teal,blue,white,hotpink,yellow,limegreen,purple',
1915
		'format' => 'bbcode',
1916
		'plugins' => '',
1917
		'bbcodeTrim' => false,
1918
	);
1919
	if (!empty($context['controls']['richedit'][$editorOptions['id']]['locale']))
1920
		$sce_options['locale'] = $context['controls']['richedit'][$editorOptions['id']]['locale'];
1921
	if (!empty($context['right_to_left']))
1922
		$sce_options['rtl'] = true;
1923
	if ($editorOptions['id'] != 'quickReply')
1924
		$sce_options['autofocus'] = true;
1925
1926
	$sce_options['emoticons'] = array();
1927
	$sce_options['emoticonsDescriptions'] = array();
1928
	$sce_options['emoticonsEnabled'] = false;
1929
	if ((!empty($context['smileys']['postform']) || !empty($context['smileys']['popup'])) && !$context['controls']['richedit'][$editorOptions['id']]['disable_smiley_box'])
1930
	{
1931
		$sce_options['emoticonsEnabled'] = true;
1932
		$sce_options['emoticons']['dropdown'] = array();
1933
		$sce_options['emoticons']['popup'] = array();
1934
1935
		$countLocations = count($context['smileys']);
1936
		foreach ($context['smileys'] as $location => $smileyRows)
1937
		{
1938
			$countLocations--;
1939
1940
			unset($smiley_location);
1941
			if ($location == 'postform')
1942
				$smiley_location = &$sce_options['emoticons']['dropdown'];
1943
			elseif ($location == 'popup')
1944
				$smiley_location = &$sce_options['emoticons']['popup'];
1945
1946
			$numRows = count($smileyRows);
1947
1948
			// This is needed because otherwise the editor will remove all the duplicate (empty) keys and leave only 1 additional line
1949
			$emptyPlaceholder = 0;
1950
			foreach ($smileyRows as $smileyRow)
1951
			{
1952
				foreach ($smileyRow['smileys'] as $smiley)
1953
				{
1954
					$smiley_location[$smiley['code']] = $settings['smileys_url'] . '/' . $smiley['filename'];
1955
					$sce_options['emoticonsDescriptions'][$smiley['code']] = $smiley['description'];
1956
				}
1957
1958
				if (empty($smileyRow['isLast']) && $numRows != 1)
1959
					$smiley_location['-' . $emptyPlaceholder++] = '';
1960
			}
1961
		}
1962
	}
1963
1964
	$sce_options['toolbar'] = '';
1965
	$sce_options['parserOptions']['txtVars'] = [
1966
		'code' => $txt['code']
1967
	];
1968
	if (!empty($modSettings['enableBBC']))
1969
	{
1970
		$count_tags = count($context['bbc_tags']);
1971
		foreach ($context['bbc_toolbar'] as $i => $buttonRow)
1972
		{
1973
			$sce_options['toolbar'] .= implode('|', $buttonRow);
1974
1975
			$count_tags--;
1976
1977
			if (!empty($count_tags))
1978
				$sce_options['toolbar'] .= '||';
1979
		}
1980
	}
1981
1982
	// Allow mods to change $sce_options. Usful if, e.g., a mod wants to add an SCEditor plugin.
1983
	call_integration_hook('integrate_sceditor_options', array(&$sce_options));
1984
1985
	$context['controls']['richedit'][$editorOptions['id']]['sce_options'] = $sce_options;
1986
}
1987
1988
/**
1989
 * Create a anti-bot verification control?
1990
 *
1991
 * @param array &$verificationOptions Options for the verification control
1992
 * @param bool $do_test Whether to check to see if the user entered the code correctly
1993
 * @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
1994
 */
1995
function create_control_verification(&$verificationOptions, $do_test = false)
1996
{
1997
	global $modSettings, $smcFunc;
1998
	global $context, $user_info, $scripturl, $language;
1999
2000
	// First verification means we need to set up some bits...
2001
	if (empty($context['controls']['verification']))
2002
	{
2003
		// The template
2004
		loadTemplate('GenericControls');
2005
2006
		// Some javascript ma'am?
2007
		if (!empty($verificationOptions['override_visual']) || (!empty($modSettings['visual_verification_type']) && !isset($verificationOptions['override_visual'])))
2008
			loadJavaScriptFile('captcha.js', array('minimize' => true), 'smf_captcha');
2009
2010
		$context['use_graphic_library'] = in_array('gd', get_loaded_extensions());
2011
2012
		// Skip I, J, L, O, Q, S and Z.
2013
		$context['standard_captcha_range'] = array_merge(range('A', 'H'), array('K', 'M', 'N', 'P', 'R'), range('T', 'Y'));
2014
	}
2015
2016
	// Always have an ID.
2017
	assert(isset($verificationOptions['id']));
2018
	$isNew = !isset($context['controls']['verification'][$verificationOptions['id']]);
2019
2020
	// Log this into our collection.
2021
	if ($isNew)
2022
		$context['controls']['verification'][$verificationOptions['id']] = array(
2023
			'id' => $verificationOptions['id'],
2024
			'empty_field' => empty($verificationOptions['no_empty_field']),
2025
			'show_visual' => !empty($verificationOptions['override_visual']) || (!empty($modSettings['visual_verification_type']) && !isset($verificationOptions['override_visual'])),
2026
			'number_questions' => isset($verificationOptions['override_qs']) ? $verificationOptions['override_qs'] : (!empty($modSettings['qa_verification_number']) ? $modSettings['qa_verification_number'] : 0),
2027
			'max_errors' => isset($verificationOptions['max_errors']) ? $verificationOptions['max_errors'] : 3,
2028
			'image_href' => $scripturl . '?action=verificationcode;vid=' . $verificationOptions['id'] . ';rand=' . md5(mt_rand()),
2029
			'text_value' => '',
2030
			'questions' => array(),
2031
			'can_recaptcha' => !empty($modSettings['recaptcha_enabled']) && !empty($modSettings['recaptcha_site_key']) && !empty($modSettings['recaptcha_secret_key']),
2032
		);
2033
	$thisVerification = &$context['controls']['verification'][$verificationOptions['id']];
2034
2035
	// Add a verification hook, presetup.
2036
	call_integration_hook('integrate_create_control_verification_pre', array(&$verificationOptions, $do_test));
2037
2038
	// Is there actually going to be anything?
2039
	if (empty($thisVerification['show_visual']) && empty($thisVerification['number_questions']) && empty($thisVerification['can_recaptcha']))
2040
		return false;
2041
	elseif (!$isNew && !$do_test)
2042
		return true;
2043
2044
	// Sanitize reCAPTCHA fields?
2045
	if ($thisVerification['can_recaptcha'])
2046
	{
2047
		// Only allow 40 alphanumeric, underscore and dash characters.
2048
		$thisVerification['recaptcha_site_key'] = preg_replace('/(0-9a-zA-Z_){40}/', '$1', $modSettings['recaptcha_site_key']);
2049
2050
		// Light or dark theme...
2051
		$thisVerification['recaptcha_theme'] = preg_replace('/(light|dark)/', '$1', $modSettings['recaptcha_theme']);
2052
	}
2053
2054
	// Add javascript for the object.
2055
	if ($context['controls']['verification'][$verificationOptions['id']]['show_visual'])
2056
		$context['insert_after_template'] .= '
2057
			<script>
2058
				var verification' . $verificationOptions['id'] . 'Handle = new smfCaptcha("' . $thisVerification['image_href'] . '", "' . $verificationOptions['id'] . '", ' . ($context['use_graphic_library'] ? 1 : 0) . ');
2059
			</script>';
2060
2061
	// If we want questions do we have a cache of all the IDs?
2062
	if (!empty($thisVerification['number_questions']) && empty($modSettings['question_id_cache']))
2063
	{
2064
		if (($modSettings['question_id_cache'] = cache_get_data('verificationQuestions', 300)) == null)
2065
		{
2066
			$request = $smcFunc['db_query']('', '
2067
				SELECT id_question, lngfile, question, answers
2068
				FROM {db_prefix}qanda',
2069
				array()
2070
			);
2071
			$modSettings['question_id_cache'] = array(
2072
				'questions' => array(),
2073
				'langs' => array(),
2074
			);
2075
			// This is like Captain Kirk climbing a mountain in some ways. This is L's fault, mkay? :P
2076
			while ($row = $smcFunc['db_fetch_assoc']($request))
2077
			{
2078
				$id_question = $row['id_question'];
2079
				unset ($row['id_question']);
2080
				// Make them all lowercase. We can't directly use $smcFunc['strtolower'] with array_walk, so do it manually, eh?
2081
				$row['answers'] = (array) $smcFunc['json_decode']($row['answers'], true);
2082
				foreach ($row['answers'] as $k => $v)
2083
					$row['answers'][$k] = $smcFunc['strtolower']($v);
2084
2085
				$modSettings['question_id_cache']['questions'][$id_question] = $row;
2086
				$modSettings['question_id_cache']['langs'][$row['lngfile']][] = $id_question;
2087
			}
2088
			$smcFunc['db_free_result']($request);
2089
2090
			cache_put_data('verificationQuestions', $modSettings['question_id_cache'], 300);
2091
		}
2092
	}
2093
2094
	if (!isset($_SESSION[$verificationOptions['id'] . '_vv']))
2095
		$_SESSION[$verificationOptions['id'] . '_vv'] = array();
2096
2097
	// Do we need to refresh the verification?
2098
	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']))
2099
		$force_refresh = true;
2100
	else
2101
		$force_refresh = false;
2102
2103
	// This can also force a fresh, although unlikely.
2104
	if (($thisVerification['show_visual'] && empty($_SESSION[$verificationOptions['id'] . '_vv']['code'])) || ($thisVerification['number_questions'] && empty($_SESSION[$verificationOptions['id'] . '_vv']['q'])))
2105
		$force_refresh = true;
2106
2107
	$verification_errors = array();
2108
	// Start with any testing.
2109
	if ($do_test)
2110
	{
2111
		// This cannot happen!
2112
		if (!isset($_SESSION[$verificationOptions['id'] . '_vv']['count']))
2113
			fatal_lang_error('no_access', false);
2114
		// ... nor this!
2115
		if ($thisVerification['number_questions'] && (!isset($_SESSION[$verificationOptions['id'] . '_vv']['q']) || !isset($_REQUEST[$verificationOptions['id'] . '_vv']['q'])))
2116
			fatal_lang_error('no_access', false);
2117
		// Hmm, it's requested but not actually declared. This shouldn't happen.
2118
		if ($thisVerification['empty_field'] && empty($_SESSION[$verificationOptions['id'] . '_vv']['empty_field']))
2119
			fatal_lang_error('no_access', false);
2120
		// While we're here, did the user do something bad?
2121
		if ($thisVerification['empty_field'] && !empty($_SESSION[$verificationOptions['id'] . '_vv']['empty_field']) && !empty($_REQUEST[$_SESSION[$verificationOptions['id'] . '_vv']['empty_field']]))
2122
			$verification_errors[] = 'wrong_verification_answer';
2123
2124
		if ($thisVerification['can_recaptcha'])
2125
		{
2126
			$reCaptcha = new \ReCaptcha\ReCaptcha($modSettings['recaptcha_secret_key'], new \ReCaptcha\RequestMethod\SocketPost());
2127
2128
			// Was there a reCAPTCHA response?
2129
			if (isset($_POST['g-recaptcha-response']))
2130
			{
2131
				$resp = $reCaptcha->verify($_POST['g-recaptcha-response'], $user_info['ip']);
2132
2133
				if (!$resp->isSuccess())
2134
					$verification_errors[] = 'wrong_verification_recaptcha';
2135
			}
2136
			else
2137
				$verification_errors[] = 'wrong_verification_code';
2138
		}
2139
		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']))
2140
			$verification_errors[] = 'wrong_verification_code';
2141
		if ($thisVerification['number_questions'])
2142
		{
2143
			$incorrectQuestions = array();
2144
			foreach ($_SESSION[$verificationOptions['id'] . '_vv']['q'] as $q)
2145
			{
2146
				// We don't have this question any more, thus no answers.
2147
				if (!isset($modSettings['question_id_cache']['questions'][$q]))
2148
					continue;
2149
				// This is quite complex. We have our question but it might have multiple answers.
2150
				// First, did they actually answer this question?
2151
				if (!isset($_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q]) || trim($_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q]) == '')
2152
				{
2153
					$incorrectQuestions[] = $q;
2154
					continue;
2155
				}
2156
				// Second, is their answer in the list of possible answers?
2157
				else
2158
				{
2159
					$given_answer = trim($smcFunc['htmlspecialchars']($smcFunc['strtolower']($_REQUEST[$verificationOptions['id'] . '_vv']['q'][$q])));
2160
					if (!in_array($given_answer, $modSettings['question_id_cache']['questions'][$q]['answers']))
2161
						$incorrectQuestions[] = $q;
2162
				}
2163
			}
2164
2165
			if (!empty($incorrectQuestions))
2166
				$verification_errors[] = 'wrong_verification_answer';
2167
		}
2168
2169
		// Hooks got anything to say about this verification?
2170
		call_integration_hook('integrate_create_control_verification_test', array($thisVerification, &$verification_errors));
2171
	}
2172
2173
	// Any errors means we refresh potentially.
2174
	if (!empty($verification_errors))
2175
	{
2176
		if (empty($_SESSION[$verificationOptions['id'] . '_vv']['errors']))
2177
			$_SESSION[$verificationOptions['id'] . '_vv']['errors'] = 0;
2178
		// Too many errors?
2179
		elseif ($_SESSION[$verificationOptions['id'] . '_vv']['errors'] > $thisVerification['max_errors'])
2180
			$force_refresh = true;
2181
2182
		// Keep a track of these.
2183
		$_SESSION[$verificationOptions['id'] . '_vv']['errors']++;
2184
	}
2185
2186
	// Are we refreshing then?
2187
	if ($force_refresh)
2188
	{
2189
		// Assume nothing went before.
2190
		$_SESSION[$verificationOptions['id'] . '_vv']['count'] = 0;
2191
		$_SESSION[$verificationOptions['id'] . '_vv']['errors'] = 0;
2192
		$_SESSION[$verificationOptions['id'] . '_vv']['did_pass'] = false;
2193
		$_SESSION[$verificationOptions['id'] . '_vv']['q'] = array();
2194
		$_SESSION[$verificationOptions['id'] . '_vv']['code'] = '';
2195
2196
		// Make our magic empty field.
2197
		if ($thisVerification['empty_field'])
2198
		{
2199
			// 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.
2200
			$terms = array('gadget', 'device', 'uid', 'gid', 'guid', 'uuid', 'unique', 'identifier');
2201
			$second_terms = array('hash', 'cipher', 'code', 'key', 'unlock', 'bit', 'value');
2202
			$start = mt_rand(0, 27);
2203
			$hash = substr(md5(time()), $start, 4);
2204
			$_SESSION[$verificationOptions['id'] . '_vv']['empty_field'] = $terms[array_rand($terms)] . '-' . $second_terms[array_rand($second_terms)] . '-' . $hash;
2205
		}
2206
2207
		// Generating a new image.
2208
		if ($thisVerification['show_visual'])
2209
		{
2210
			// Are we overriding the range?
2211
			$character_range = !empty($verificationOptions['override_range']) ? $verificationOptions['override_range'] : $context['standard_captcha_range'];
2212
2213
			for ($i = 0; $i < 6; $i++)
2214
				$_SESSION[$verificationOptions['id'] . '_vv']['code'] .= $character_range[array_rand($character_range)];
2215
		}
2216
2217
		// Getting some new questions?
2218
		if ($thisVerification['number_questions'])
2219
		{
2220
			// Attempt to try the current page's language, followed by the user's preference, followed by the site default.
2221
			$possible_langs = array();
2222
			if (isset($_SESSION['language']))
2223
				$possible_langs[] = strtr($_SESSION['language'], array('-utf8' => ''));
2224
			if (!empty($user_info['language']))
2225
				$possible_langs[] = $user_info['language'];
2226
2227
			$possible_langs[] = $language;
2228
2229
			$questionIDs = array();
2230
			foreach ($possible_langs as $lang)
2231
			{
2232
				$lang = strtr($lang, array('-utf8' => ''));
2233
				if (isset($modSettings['question_id_cache']['langs'][$lang]))
2234
				{
2235
					// If we find questions for this, grab the ids from this language's ones, randomize the array and take just the number we need.
2236
					$questionIDs = $modSettings['question_id_cache']['langs'][$lang];
2237
					shuffle($questionIDs);
2238
					$questionIDs = array_slice($questionIDs, 0, $thisVerification['number_questions']);
2239
					break;
2240
				}
2241
			}
2242
		}
2243
2244
		// Hooks may need to know about this.
2245
		call_integration_hook('integrate_create_control_verification_refresh', array($thisVerification));
2246
	}
2247
	else
2248
	{
2249
		// Same questions as before.
2250
		$questionIDs = !empty($_SESSION[$verificationOptions['id'] . '_vv']['q']) ? $_SESSION[$verificationOptions['id'] . '_vv']['q'] : array();
2251
		$thisVerification['text_value'] = !empty($_REQUEST[$verificationOptions['id'] . '_vv']['code']) ? $smcFunc['htmlspecialchars']($_REQUEST[$verificationOptions['id'] . '_vv']['code']) : '';
2252
	}
2253
2254
	// If we do have an empty field, it would be nice to hide it from legitimate users who shouldn't be populating it anyway.
2255
	if (!empty($_SESSION[$verificationOptions['id'] . '_vv']['empty_field']))
2256
	{
2257
		if (!isset($context['html_headers']))
2258
			$context['html_headers'] = '';
2259
		$context['html_headers'] .= '<style>.vv_special { display:none; }</style>';
2260
	}
2261
2262
	// Have we got some questions to load?
2263
	if (!empty($questionIDs))
2264
	{
2265
		$_SESSION[$verificationOptions['id'] . '_vv']['q'] = array();
2266
		foreach ($questionIDs as $q)
2267
		{
2268
			// Bit of a shortcut this.
2269
			$row = &$modSettings['question_id_cache']['questions'][$q];
2270
			$thisVerification['questions'][] = array(
2271
				'id' => $q,
2272
				'q' => parse_bbc($row['question']),
2273
				'is_error' => !empty($incorrectQuestions) && in_array($q, $incorrectQuestions),
2274
				// Remember a previous submission?
2275
				'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]) : '',
2276
			);
2277
			$_SESSION[$verificationOptions['id'] . '_vv']['q'][] = $q;
2278
		}
2279
	}
2280
2281
	$_SESSION[$verificationOptions['id'] . '_vv']['count'] = empty($_SESSION[$verificationOptions['id'] . '_vv']['count']) ? 1 : $_SESSION[$verificationOptions['id'] . '_vv']['count'] + 1;
2282
2283
	// Let our hooks know that we are done with the verification process.
2284
	call_integration_hook('integrate_create_control_verification_post', array(&$verification_errors, $do_test));
2285
2286
	// Return errors if we have them.
2287
	if (!empty($verification_errors))
2288
		return $verification_errors;
2289
	// If we had a test that one, make a note.
2290
	elseif ($do_test)
2291
		$_SESSION[$verificationOptions['id'] . '_vv']['did_pass'] = true;
2292
2293
	// Say that everything went well chaps.
2294
	return true;
2295
}
2296
2297
/**
2298
 * This keeps track of all registered handling functions for auto suggest functionality and passes execution to them.
2299
 *
2300
 * @param bool $checkRegistered If set to something other than null, checks whether the callback function is registered
2301
 * @return void|bool Returns whether the callback function is registered if $checkRegistered isn't null
2302
 */
2303
function AutoSuggestHandler($checkRegistered = null)
2304
{
2305
	global $smcFunc, $context;
2306
2307
	// These are all registered types.
2308
	$searchTypes = array(
2309
		'member' => 'Member',
2310
		'membergroups' => 'MemberGroups',
2311
		'versions' => 'SMFVersions',
2312
	);
2313
2314
	call_integration_hook('integrate_autosuggest', array(&$searchTypes));
2315
2316
	// If we're just checking the callback function is registered return true or false.
2317
	if ($checkRegistered != null)
2318
		return isset($searchTypes[$checkRegistered]) && function_exists('AutoSuggest_Search_' . $checkRegistered);
0 ignored issues
show
Are you sure $checkRegistered of type true can be used in concatenation? ( Ignorable by Annotation )

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

2318
		return isset($searchTypes[$checkRegistered]) && function_exists('AutoSuggest_Search_' . /** @scrutinizer ignore-type */ $checkRegistered);
Loading history...
2319
2320
	checkSession('get');
2321
	loadTemplate('Xml');
2322
2323
	// Any parameters?
2324
	$context['search_param'] = isset($_REQUEST['search_param']) ? $smcFunc['json_decode'](base64_decode($_REQUEST['search_param']), true) : array();
2325
2326
	if (isset($_REQUEST['suggest_type'], $_REQUEST['search']) && isset($searchTypes[$_REQUEST['suggest_type']]))
2327
	{
2328
		$function = 'AutoSuggest_Search_' . $searchTypes[$_REQUEST['suggest_type']];
2329
		$context['sub_template'] = 'generic_xml';
2330
		$context['xml_data'] = $function();
2331
	}
2332
}
2333
2334
/**
2335
 * Search for a member - by real_name or member_name by default.
2336
 *
2337
 * @return array An array of information for displaying the suggestions
2338
 */
2339
function AutoSuggest_Search_Member()
2340
{
2341
	global $user_info, $smcFunc, $context;
2342
2343
	$_REQUEST['search'] = trim($smcFunc['strtolower']($_REQUEST['search'])) . '*';
2344
	$_REQUEST['search'] = strtr($_REQUEST['search'], array('%' => '\%', '_' => '\_', '*' => '%', '?' => '_', '&#038;' => '&amp;'));
2345
2346
	// Find the member.
2347
	$request = $smcFunc['db_query']('', '
2348
		SELECT id_member, real_name
2349
		FROM {db_prefix}members
2350
		WHERE {raw:real_name} LIKE {string:search}' . (!empty($context['search_param']['buddies']) ? '
2351
			AND id_member IN ({array_int:buddy_list})' : '') . '
2352
			AND is_activated IN (1, 11)
2353
		LIMIT ' . ($smcFunc['strlen']($_REQUEST['search']) <= 2 ? '100' : '800'),
2354
		array(
2355
			'real_name' => $smcFunc['db_case_sensitive'] ? 'LOWER(real_name)' : 'real_name',
2356
			'buddy_list' => $user_info['buddies'],
2357
			'search' => $_REQUEST['search'],
2358
		)
2359
	);
2360
	$xml_data = array(
2361
		'items' => array(
2362
			'identifier' => 'item',
2363
			'children' => array(),
2364
		),
2365
	);
2366
	while ($row = $smcFunc['db_fetch_assoc']($request))
2367
	{
2368
		$row['real_name'] = strtr($row['real_name'], array('&amp;' => '&#038;', '&lt;' => '&#060;', '&gt;' => '&#062;', '&quot;' => '&#034;'));
2369
2370
		$xml_data['items']['children'][] = array(
2371
			'attributes' => array(
2372
				'id' => $row['id_member'],
2373
			),
2374
			'value' => $row['real_name'],
2375
		);
2376
	}
2377
	$smcFunc['db_free_result']($request);
2378
2379
	return $xml_data;
2380
}
2381
2382
/**
2383
 * Search for a membergroup by name
2384
 *
2385
 * @return array An array of information for displaying the suggestions
2386
 */
2387
function AutoSuggest_Search_MemberGroups()
2388
{
2389
	global $smcFunc;
2390
2391
	$_REQUEST['search'] = trim($smcFunc['strtolower']($_REQUEST['search'])) . '*';
2392
	$_REQUEST['search'] = strtr($_REQUEST['search'], array('%' => '\%', '_' => '\_', '*' => '%', '?' => '_', '&#038;' => '&amp;'));
2393
2394
	// Find the group.
2395
	// Only return groups which are not post-based and not "Hidden", but not the "Administrators" or "Moderators" groups.
2396
	$request = $smcFunc['db_query']('', '
2397
		SELECT id_group, group_name
2398
		FROM {db_prefix}membergroups
2399
		WHERE {raw:group_name} LIKE {string:search}
2400
			AND min_posts = {int:min_posts}
2401
			AND id_group NOT IN ({array_int:invalid_groups})
2402
			AND hidden != {int:hidden}',
2403
		array(
2404
			'group_name' => $smcFunc['db_case_sensitive'] ? 'LOWER(group_name)' : 'group_name',
2405
			'min_posts' => -1,
2406
			'invalid_groups' => array(1, 3),
2407
			'hidden' => 2,
2408
			'search' => $_REQUEST['search'],
2409
		)
2410
	);
2411
	$xml_data = array(
2412
		'items' => array(
2413
			'identifier' => 'item',
2414
			'children' => array(),
2415
		),
2416
	);
2417
	while ($row = $smcFunc['db_fetch_assoc']($request))
2418
	{
2419
		$row['group_name'] = strtr($row['group_name'], array('&amp;' => '&#038;', '&lt;' => '&#060;', '&gt;' => '&#062;', '&quot;' => '&#034;'));
2420
2421
		$xml_data['items']['children'][] = array(
2422
			'attributes' => array(
2423
				'id' => $row['id_group'],
2424
			),
2425
			'value' => $row['group_name'],
2426
		);
2427
	}
2428
	$smcFunc['db_free_result']($request);
2429
2430
	return $xml_data;
2431
}
2432
2433
/**
2434
 * Provides a list of possible SMF versions to use in emulation
2435
 *
2436
 * @return array An array of data for displaying the suggestions
2437
 */
2438
function AutoSuggest_Search_SMFVersions()
2439
{
2440
	global $smcFunc;
2441
2442
	$xml_data = array(
2443
		'items' => array(
2444
			'identifier' => 'item',
2445
			'children' => array(),
2446
		),
2447
	);
2448
2449
	// First try and get it from the database.
2450
	$versions = array();
2451
	$request = $smcFunc['db_query']('', '
2452
		SELECT data
2453
		FROM {db_prefix}admin_info_files
2454
		WHERE filename = {string:latest_versions}
2455
			AND path = {string:path}',
2456
		array(
2457
			'latest_versions' => 'latest-versions.txt',
2458
			'path' => '/smf/',
2459
		)
2460
	);
2461
	if (($smcFunc['db_num_rows']($request) > 0) && ($row = $smcFunc['db_fetch_assoc']($request)) && !empty($row['data']))
2462
	{
2463
		// The file can be either Windows or Linux line endings, but let's ensure we clean it as best we can.
2464
		$possible_versions = explode("\n", $row['data']);
2465
		foreach ($possible_versions as $ver)
2466
		{
2467
			$ver = trim($ver);
2468
			if (strpos($ver, 'SMF') === 0)
2469
				$versions[] = $ver;
2470
		}
2471
	}
2472
	$smcFunc['db_free_result']($request);
2473
2474
	// Just in case we don't have ANYthing.
2475
	if (empty($versions))
2476
		$versions = array(SMF_FULL_VERSION);
2477
2478
	foreach ($versions as $id => $version)
2479
		if (strpos($version, strtoupper($_REQUEST['search'])) !== false)
2480
			$xml_data['items']['children'][] = array(
2481
				'attributes' => array(
2482
					'id' => $id,
2483
				),
2484
				'value' => $version,
2485
			);
2486
2487
	return $xml_data;
2488
}
2489
2490
?>