|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
/** |
|
4
|
|
|
* This class contains those functions pertaining to preparsing BBC data |
|
5
|
|
|
* |
|
6
|
|
|
* @name ElkArte Forum |
|
7
|
|
|
* @copyright ElkArte Forum contributors |
|
8
|
|
|
* @license BSD http://opensource.org/licenses/BSD-3-Clause |
|
9
|
|
|
* |
|
10
|
|
|
* This file contains code covered by: |
|
11
|
|
|
* copyright: 2011 Simple Machines (http://www.simplemachines.org) |
|
12
|
|
|
* license: BSD, See included LICENSE.TXT for terms and conditions. |
|
13
|
|
|
* |
|
14
|
|
|
* @version 2.0 dev |
|
15
|
|
|
* |
|
16
|
|
|
*/ |
|
17
|
|
|
|
|
18
|
|
|
namespace BBC; |
|
19
|
|
|
|
|
20
|
|
|
/** |
|
21
|
|
|
* Class PreparseCode |
|
22
|
|
|
* |
|
23
|
|
|
* @package BBC |
|
24
|
|
|
*/ |
|
25
|
|
|
class PreparseCode |
|
26
|
|
|
{ |
|
27
|
|
|
/** The regular expression non breaking space */ |
|
28
|
|
|
const NBS = '\x{A0}'; |
|
29
|
|
|
/** @var string the message to preparse */ |
|
30
|
|
|
public $message = ''; |
|
31
|
|
|
/** @var bool if this is just a preview */ |
|
32
|
|
|
protected $previewing = false; |
|
33
|
|
|
/** @var array the code blocks that we want to protect */ |
|
34
|
|
|
public $code_blocks = array(); |
|
35
|
|
|
/** @var PreparseCode */ |
|
36
|
|
|
public static $instance; |
|
37
|
|
|
|
|
38
|
|
|
/** |
|
39
|
|
|
* PreparseCode constructor. |
|
40
|
|
|
*/ |
|
41
|
2 |
|
public function __construct() |
|
42
|
|
|
{ |
|
43
|
2 |
|
} |
|
44
|
|
|
|
|
45
|
|
|
/** |
|
46
|
|
|
* Takes a message and parses it, returning the prepared message as a reference |
|
47
|
|
|
* for use by parse_bbc. |
|
48
|
|
|
* |
|
49
|
|
|
* What it does: |
|
50
|
|
|
* - Cleans up links (javascript, etc.) |
|
51
|
|
|
* - Fixes improperly constructed lists [lists] |
|
52
|
|
|
* - Repairs improperly constructed tables, row, headers, etc |
|
53
|
|
|
* - Protects code sections |
|
54
|
|
|
* - Checks for proper quote open / closing |
|
55
|
|
|
* - Processes /me tag |
|
56
|
|
|
* - Converts color tags to ones parse_bbc will understand |
|
57
|
|
|
* - Removes empty tags outside of code blocks |
|
58
|
|
|
* - Won't convert \n's and a few other things if previewing is true. |
|
59
|
|
|
* |
|
60
|
|
|
* @param string $message |
|
61
|
|
|
* @param boolean $previewing |
|
62
|
|
|
*/ |
|
63
|
3 |
|
public function preparsecode(&$message, $previewing = false) |
|
64
|
|
|
{ |
|
65
|
|
|
// Load passed values to the class |
|
66
|
3 |
|
$this->message = $message; |
|
67
|
3 |
|
$this->previewing = $previewing; |
|
68
|
|
|
|
|
69
|
|
|
// This line makes all languages *theoretically* work even with the wrong charset ;). |
|
70
|
3 |
|
$this->message = preg_replace('~&#(\d{4,5}|[2-9]\d{2,4}|1[2-9]\d);~', '&#$1;', $this->message); |
|
71
|
|
|
|
|
72
|
|
|
// Clean up after nobbc ;). |
|
73
|
3 |
|
$this->message = preg_replace_callback('~\[nobbc\](.+?)\[/nobbc\]~i', array($this, '_preparsecode_nobbc_callback'), $this->message); |
|
74
|
|
|
|
|
75
|
|
|
// Remove \r's... they're evil! |
|
76
|
3 |
|
$this->message = strtr($this->message, array("\r" => '')); |
|
77
|
|
|
|
|
78
|
|
|
// You won't believe this - but too many periods upsets apache it seems! |
|
79
|
3 |
|
$this->message = preg_replace('~\.{100,}~', '...', $this->message); |
|
80
|
|
|
|
|
81
|
|
|
// Remove Trailing Quotes |
|
82
|
3 |
|
$this->_trimTrailingQuotes(); |
|
83
|
|
|
|
|
84
|
|
|
// Validate code blocks are properly closed. |
|
85
|
3 |
|
$this->_validateCodeBlocks(); |
|
86
|
|
|
|
|
87
|
|
|
// Protect CODE blocks from further processing |
|
88
|
3 |
|
$this->_tokenizeCodeBlocks(); |
|
89
|
|
|
|
|
90
|
|
|
// Now that we've fixed all the code tags, let's fix the img and url tags... |
|
91
|
3 |
|
$this->_fixTags(); |
|
92
|
|
|
|
|
93
|
|
|
// Replace /me.+?\n with [me=name]dsf[/me]\n. |
|
94
|
3 |
|
$this->_itsAllAbout(); |
|
95
|
|
|
|
|
96
|
|
|
// Make sure list and table tags are lowercase. |
|
97
|
3 |
|
$this->message = preg_replace_callback('~\[([/]?)(list|li|table|tr|td|th)((\s[^\]]+)*)\]~i', array($this, '_preparsecode_lowertags_callback'), $this->message); |
|
98
|
|
|
|
|
99
|
|
|
// Don't leave any lists that were never opened or closed |
|
100
|
3 |
|
$this->_validateLists(); |
|
101
|
|
|
|
|
102
|
|
|
// Attempt to repair common BBC input mistakes |
|
103
|
3 |
|
$this->_fixMistakes(); |
|
104
|
|
|
|
|
105
|
|
|
// Remove empty bbc tags |
|
106
|
3 |
|
$this->message = preg_replace('~\[[bisu]\]\s*\[/[bisu]\]~', '', $this->message); |
|
107
|
3 |
|
$this->message = preg_replace('~\[quote\]\s*\[/quote\]~', '', $this->message); |
|
108
|
|
|
|
|
109
|
|
|
// Fix color tags of many forms so they parse properly |
|
110
|
3 |
|
$this->message = preg_replace('~\[color=(?:#[\da-fA-F]{3}|#[\da-fA-F]{6}|[A-Za-z]{1,20}|rgb\(\d{1,3}, ?\d{1,3}, ?\d{1,3}\))\]\s*\[/color\]~', '', $this->message); |
|
111
|
|
|
|
|
112
|
|
|
// Font tags with multiple fonts (copy&paste in the WYSIWYG by some browsers). |
|
113
|
3 |
|
$this->message = preg_replace_callback('~\[font=([^\]]*)\](.*?(?:\[/font\]))~s', array($this, '_preparsecode_font_callback'), $this->message); |
|
114
|
|
|
|
|
115
|
|
|
// Allow integration to do further processing on protected code block message |
|
116
|
3 |
|
call_integration_hook('integrate_preparse_tokenized_code', array(&$this->message, $previewing, $this->code_blocks)); |
|
117
|
|
|
|
|
118
|
|
|
// Put it back together! |
|
119
|
3 |
|
$this->_restoreCodeBlocks(); |
|
120
|
|
|
|
|
121
|
|
|
// Allow integration to do further processing |
|
122
|
3 |
|
call_integration_hook('integrate_preparse_code', array(&$this->message, 0, $previewing)); |
|
123
|
|
|
|
|
124
|
|
|
// Safe Spacing |
|
125
|
3 |
|
if (!$previewing) |
|
126
|
|
|
{ |
|
127
|
3 |
|
$this->message = strtr($this->message, array(' ' => ' ', "\n" => '<br />', "\xC2\xA0" => ' ')); |
|
128
|
|
|
} |
|
129
|
|
|
else |
|
130
|
|
|
{ |
|
131
|
|
|
$this->message = strtr($this->message, array(' ' => ' ', "\xC2\xA0" => ' ')); |
|
132
|
|
|
} |
|
133
|
|
|
|
|
134
|
|
|
// Now we're going to do full scale table checking... |
|
135
|
3 |
|
$this->_preparseTable(); |
|
136
|
|
|
|
|
137
|
|
|
// Quickly clean up things that will slow our parser (which are common in posted code.) |
|
138
|
3 |
|
$message = strtr($this->message, array('[]' => '[]', '['' => '['')); |
|
139
|
3 |
|
} |
|
140
|
|
|
|
|
141
|
|
|
/** |
|
142
|
|
|
* Trim dangling quotes |
|
143
|
|
|
*/ |
|
144
|
3 |
|
private function _trimTrailingQuotes() |
|
145
|
|
|
{ |
|
146
|
|
|
// Trim off trailing quotes - these often happen by accident. |
|
147
|
3 |
View Code Duplication |
while (substr($this->message, -7) === '[quote]') |
|
148
|
|
|
{ |
|
149
|
|
|
$this->message = trim(substr($this->message, 0, -7)); |
|
150
|
|
|
} |
|
151
|
|
|
|
|
152
|
|
|
// Trim off leading ones as well |
|
153
|
3 |
View Code Duplication |
while (substr($this->message, 0, 8) === '[/quote]') |
|
154
|
|
|
{ |
|
155
|
|
|
$this->message = trim(substr($this->message, 8)); |
|
156
|
|
|
} |
|
157
|
3 |
|
} |
|
158
|
|
|
|
|
159
|
|
|
/** |
|
160
|
|
|
* Find all code blocks, work out whether we'd be parsing them, |
|
161
|
|
|
* then ensure they are all closed. |
|
162
|
|
|
*/ |
|
163
|
3 |
|
private function _validateCodeBlocks() |
|
164
|
|
|
{ |
|
165
|
3 |
|
$in_tag = false; |
|
166
|
3 |
|
$had_tag = false; |
|
167
|
3 |
|
$code_open = false; |
|
168
|
|
|
|
|
169
|
3 |
|
if (preg_match_all('~(\[(/)*code(?:=[^\]]+)?\])~is', $this->message, $matches)) |
|
170
|
|
|
{ |
|
171
|
2 |
|
foreach ($matches[0] as $index => $dummy) |
|
172
|
|
|
{ |
|
173
|
|
|
// Closing? |
|
174
|
2 |
|
if (!empty($matches[2][$index])) |
|
175
|
|
|
{ |
|
176
|
|
|
// If it's closing and we're not in a tag we need to open it... |
|
177
|
2 |
|
if (!$in_tag) |
|
178
|
|
|
{ |
|
179
|
|
|
$code_open = true; |
|
180
|
|
|
} |
|
181
|
|
|
|
|
182
|
|
|
// Either way we ain't in one any more. |
|
183
|
2 |
|
$in_tag = false; |
|
184
|
|
|
} |
|
185
|
|
|
// Opening tag... |
|
186
|
|
|
else |
|
187
|
|
|
{ |
|
188
|
2 |
|
$had_tag = true; |
|
189
|
|
|
|
|
190
|
|
|
// If we're in a tag don't do nought! |
|
191
|
2 |
|
if (!$in_tag) |
|
192
|
|
|
{ |
|
193
|
2 |
|
$in_tag = true; |
|
194
|
|
|
} |
|
195
|
|
|
} |
|
196
|
|
|
} |
|
197
|
|
|
} |
|
198
|
|
|
|
|
199
|
|
|
// If we have an open code tag, close it. |
|
200
|
3 |
|
if ($in_tag) |
|
201
|
|
|
{ |
|
202
|
2 |
|
$this->message .= '[/code]'; |
|
203
|
|
|
} |
|
204
|
|
|
|
|
205
|
|
|
// Open any ones that need to be open, only if we've never had a tag. |
|
206
|
3 |
|
if ($code_open && !$had_tag) |
|
207
|
|
|
{ |
|
208
|
|
|
$this->message = '[code]' . $this->message; |
|
209
|
|
|
} |
|
210
|
3 |
|
} |
|
211
|
|
|
|
|
212
|
|
|
/** |
|
213
|
|
|
* Protects code blocks from preparse by replacing them with %%token%% values |
|
214
|
|
|
*/ |
|
215
|
3 |
|
private function _tokenizeCodeBlocks() |
|
216
|
|
|
{ |
|
217
|
|
|
// Split up the message on the code start/end tags/ |
|
218
|
3 |
|
$parts = preg_split('~(\[/code\]|\[code(?:=[^\]]+)?\])~i', $this->message, -1, PREG_SPLIT_DELIM_CAPTURE); |
|
219
|
|
|
|
|
220
|
|
|
// Token generator |
|
221
|
3 |
|
$tokenizer = new \Token_Hash(); |
|
222
|
|
|
|
|
223
|
|
|
// Separate all code blocks |
|
224
|
3 |
|
for ($i = 0, $n = count($parts); $i < $n; $i++) |
|
225
|
|
|
{ |
|
226
|
|
|
// It goes 0 = outside, 1 = begin tag, 2 = inside, 3 = close tag, repeat. |
|
227
|
3 |
|
if ($i % 4 === 0 && isset($parts[$i + 3])) |
|
228
|
|
|
{ |
|
229
|
|
|
// Create a unique key to put in place of the code block |
|
230
|
2 |
|
$key = $tokenizer->generate_hash(8); |
|
231
|
|
|
|
|
232
|
|
|
// Save what is there [code]stuff[/code] |
|
233
|
2 |
|
$this->code_blocks['%%' . $key . '%%'] = $parts[$i + 1] . $parts[$i + 2] . $parts[$i + 3]; |
|
234
|
|
|
|
|
235
|
|
|
// Replace the code block with %%$key%% so its protected from further preparsecode processing |
|
236
|
2 |
|
$parts[$i + 1] = '%%'; |
|
237
|
2 |
|
$parts[$i + 2] = $key; |
|
238
|
2 |
|
$parts[$i + 3] = '%%'; |
|
239
|
|
|
} |
|
240
|
|
|
} |
|
241
|
|
|
|
|
242
|
|
|
// The message with code blocks as %%tokens%% |
|
243
|
3 |
|
$this->message = implode('', $parts); |
|
244
|
3 |
|
} |
|
245
|
|
|
|
|
246
|
|
|
/** |
|
247
|
|
|
* Fix any URLs posted - ie. remove 'javascript:'. |
|
248
|
|
|
* |
|
249
|
|
|
* - Fix the img and url tags... |
|
250
|
|
|
* - Fixes links in message and returns nothing. |
|
251
|
|
|
*/ |
|
252
|
3 |
|
private function _fixTags() |
|
253
|
|
|
{ |
|
254
|
3 |
|
global $modSettings; |
|
255
|
|
|
|
|
256
|
|
|
// WARNING: Editing the below can cause large security holes in your forum. |
|
257
|
|
|
// Edit only if you are sure you know what you are doing. |
|
258
|
|
|
|
|
259
|
|
|
$fixArray = array( |
|
260
|
|
|
// [img]http://...[/img] or [img width=1]http://...[/img] |
|
261
|
|
|
array( |
|
262
|
3 |
|
'tag' => 'img', |
|
263
|
|
|
'protocols' => array('http', 'https'), |
|
264
|
|
|
'embeddedUrl' => false, |
|
265
|
|
|
'hasEqualSign' => false, |
|
266
|
|
|
'hasExtra' => true, |
|
267
|
|
|
), |
|
268
|
|
|
// [url]http://...[/url] |
|
269
|
|
|
array( |
|
270
|
|
|
'tag' => 'url', |
|
271
|
|
|
'protocols' => array('http', 'https'), |
|
272
|
|
|
'embeddedUrl' => true, |
|
273
|
|
|
'hasEqualSign' => false, |
|
274
|
|
|
), |
|
275
|
|
|
// [url=http://...]name[/url] |
|
276
|
|
|
array( |
|
277
|
|
|
'tag' => 'url', |
|
278
|
|
|
'protocols' => array('http', 'https'), |
|
279
|
|
|
'embeddedUrl' => true, |
|
280
|
|
|
'hasEqualSign' => true, |
|
281
|
|
|
), |
|
282
|
|
|
// [iurl]http://...[/iurl] |
|
283
|
|
|
array( |
|
284
|
|
|
'tag' => 'iurl', |
|
285
|
|
|
'protocols' => array('http', 'https'), |
|
286
|
|
|
'embeddedUrl' => true, |
|
287
|
|
|
'hasEqualSign' => false, |
|
288
|
|
|
), |
|
289
|
|
|
// [iurl=http://...]name[/iurl] |
|
290
|
|
|
array( |
|
291
|
|
|
'tag' => 'iurl', |
|
292
|
|
|
'protocols' => array('http', 'https'), |
|
293
|
|
|
'embeddedUrl' => true, |
|
294
|
|
|
'hasEqualSign' => true, |
|
295
|
|
|
), |
|
296
|
|
|
); |
|
297
|
|
|
|
|
298
|
|
|
// Integration may want to add to this array |
|
299
|
3 |
|
call_integration_hook('integrate_fixtags', array(&$fixArray, &$this->message)); |
|
300
|
|
|
|
|
301
|
|
|
// Fix each type of tag. |
|
302
|
3 |
|
foreach ($fixArray as $param) |
|
303
|
|
|
{ |
|
304
|
3 |
|
$this->_fixTag($param['tag'], $param['protocols'], $param['embeddedUrl'], $param['hasEqualSign'], !empty($param['hasExtra'])); |
|
305
|
|
|
} |
|
306
|
|
|
|
|
307
|
|
|
// Now fix possible security problems with images loading links automatically... |
|
308
|
3 |
|
$this->message = preg_replace_callback('~(\[img.*?\])(.+?)\[/img\]~is', array($this, '_fixTags_img_callback'), $this->message); |
|
309
|
|
|
|
|
310
|
|
|
// Limit the size of images posted? |
|
311
|
3 |
|
if (!empty($modSettings['max_image_width']) || !empty($modSettings['max_image_height'])) |
|
312
|
|
|
{ |
|
313
|
|
|
$this->resizeBBCImages(); |
|
314
|
|
|
} |
|
315
|
3 |
|
} |
|
316
|
|
|
|
|
317
|
|
|
/** |
|
318
|
|
|
* Fix a specific class of tag - ie. url with =. |
|
319
|
|
|
* |
|
320
|
|
|
* - Used by fixTags, fixes a specific tag's links. |
|
321
|
|
|
* |
|
322
|
|
|
* @param string $myTag - the tag |
|
323
|
|
|
* @param string[] $protocols - http, https or ftp |
|
324
|
|
|
* @param bool $embeddedUrl = false - whether it *can* be set to something |
|
325
|
|
|
* @param bool $hasEqualSign = false, whether it *is* set to something |
|
326
|
|
|
* @param bool $hasExtra = false - whether it can have extra cruft after the begin tag. |
|
327
|
|
|
*/ |
|
328
|
3 |
|
private function _fixTag($myTag, $protocols, $embeddedUrl = false, $hasEqualSign = false, $hasExtra = false) |
|
329
|
|
|
{ |
|
330
|
3 |
|
global $boardurl, $scripturl; |
|
331
|
|
|
|
|
332
|
3 |
|
$replaces = array(); |
|
333
|
|
|
|
|
334
|
|
|
// Ensure it has a domain name, use the site name if needed |
|
335
|
3 |
|
if (preg_match('~^([^:]+://[^/]+)~', $boardurl, $match) != 0) |
|
336
|
|
|
{ |
|
337
|
3 |
|
$domain_url = $match[1]; |
|
338
|
|
|
} |
|
339
|
|
|
else |
|
340
|
|
|
{ |
|
341
|
|
|
$domain_url = $boardurl . '/'; |
|
342
|
|
|
} |
|
343
|
|
|
|
|
344
|
3 |
|
if ($hasEqualSign) |
|
345
|
|
|
{ |
|
346
|
3 |
|
preg_match_all('~\[(' . $myTag . ')=([^\]]*?)\](?:(.+?)\[/(' . $myTag . ')\])?~is', $this->message, $matches); |
|
347
|
|
|
} |
|
348
|
|
|
else |
|
349
|
|
|
{ |
|
350
|
3 |
|
preg_match_all('~\[(' . $myTag . ($hasExtra ? '(?:[^\]]*?)' : '') . ')\](.+?)\[/(' . $myTag . ')\]~is', $this->message, $matches); |
|
351
|
|
|
} |
|
352
|
|
|
|
|
353
|
3 |
|
foreach ($matches[0] as $k => $dummy) |
|
354
|
|
|
{ |
|
355
|
|
|
// Remove all leading and trailing whitespace. |
|
356
|
1 |
|
$replace = trim($matches[2][$k]); |
|
357
|
1 |
|
$this_tag = $matches[1][$k]; |
|
358
|
1 |
|
$this_close = $hasEqualSign ? (empty($matches[4][$k]) ? '' : $matches[4][$k]) : $matches[3][$k]; |
|
359
|
|
|
|
|
360
|
1 |
|
$found = false; |
|
361
|
1 |
|
foreach ($protocols as $protocol) |
|
362
|
|
|
{ |
|
363
|
1 |
|
$found = strncasecmp($replace, $protocol . '://', strlen($protocol) + 3) === 0; |
|
364
|
1 |
|
if ($found) |
|
365
|
|
|
{ |
|
366
|
1 |
|
break; |
|
367
|
|
|
} |
|
368
|
|
|
} |
|
369
|
|
|
|
|
370
|
|
|
// Http url checking? |
|
371
|
1 |
|
if (!$found && $protocols[0] === 'http') |
|
372
|
|
|
{ |
|
373
|
1 |
|
if (substr($replace, 0, 1) === '/' && substr($replace, 0, 2) !== '//') |
|
374
|
|
|
{ |
|
375
|
|
|
$replace = $domain_url . $replace; |
|
376
|
|
|
} |
|
377
|
1 |
|
elseif (substr($replace, 0, 1) === '?') |
|
378
|
|
|
{ |
|
379
|
|
|
$replace = $scripturl . $replace; |
|
380
|
|
|
} |
|
381
|
1 |
|
elseif (substr($replace, 0, 1) === '#' && $embeddedUrl) |
|
382
|
|
|
{ |
|
383
|
|
|
$replace = '#' . preg_replace('~[^A-Za-z0-9_\-#]~', '', substr($replace, 1)); |
|
384
|
|
|
$this_tag = 'iurl'; |
|
385
|
|
|
$this_close = 'iurl'; |
|
386
|
|
|
} |
|
387
|
1 |
|
elseif (substr($replace, 0, 2) === '//') |
|
388
|
|
|
{ |
|
389
|
1 |
|
$replace = $protocols[0] . ':' . $replace; |
|
390
|
|
|
} |
|
391
|
|
|
else |
|
392
|
|
|
{ |
|
393
|
1 |
|
$replace = $protocols[0] . '://' . $replace; |
|
394
|
|
|
} |
|
395
|
|
|
} |
|
396
|
|
|
// FTP URL Checking |
|
397
|
1 |
|
elseif (!$found && $protocols[0] === 'ftp') |
|
398
|
|
|
{ |
|
399
|
|
|
$replace = $protocols[0] . '://' . preg_replace('~^(?!ftps?)[^:]+://~', '', $replace); |
|
400
|
|
|
} |
|
401
|
1 |
|
elseif (!$found) |
|
402
|
|
|
{ |
|
403
|
|
|
$replace = $protocols[0] . '://' . $replace; |
|
404
|
|
|
} |
|
405
|
|
|
|
|
406
|
|
|
// Build a replacement array that is considered safe and proper |
|
407
|
1 |
|
if ($hasEqualSign && $embeddedUrl) |
|
408
|
|
|
{ |
|
409
|
1 |
|
$replaces[$matches[0][$k]] = '[' . $this_tag . '=' . $replace . ']' . (empty($matches[4][$k]) ? '' : $matches[3][$k] . '[/' . $this_close . ']'); |
|
410
|
|
|
} |
|
411
|
1 |
|
elseif ($hasEqualSign) |
|
412
|
|
|
{ |
|
413
|
|
|
$replaces['[' . $matches[1][$k] . '=' . $matches[2][$k] . ']'] = '[' . $this_tag . '=' . $replace . ']'; |
|
414
|
|
|
} |
|
415
|
1 |
|
elseif ($embeddedUrl) |
|
416
|
|
|
{ |
|
417
|
1 |
|
$replaces['[' . $matches[1][$k] . ']' . $matches[2][$k] . '[/' . $matches[3][$k] . ']'] = '[' . $this_tag . '=' . $replace . ']' . $matches[2][$k] . '[/' . $this_close . ']'; |
|
418
|
|
|
} |
|
419
|
|
|
else |
|
420
|
|
|
{ |
|
421
|
1 |
|
$replaces['[' . $matches[1][$k] . ']' . $matches[2][$k] . '[/' . $matches[3][$k] . ']'] = '[' . $this_tag . ']' . $replace . '[/' . $this_close . ']'; |
|
422
|
|
|
} |
|
423
|
|
|
} |
|
424
|
|
|
|
|
425
|
3 |
|
foreach ($replaces as $k => $v) |
|
426
|
|
|
{ |
|
427
|
1 |
|
if ($k == $v) |
|
428
|
|
|
{ |
|
429
|
1 |
|
unset($replaces[$k]); |
|
430
|
|
|
} |
|
431
|
|
|
} |
|
432
|
|
|
|
|
433
|
|
|
// Update as needed |
|
434
|
3 |
|
if (!empty($replaces)) |
|
435
|
|
|
{ |
|
436
|
1 |
|
$this->message = strtr($this->message, $replaces); |
|
437
|
|
|
} |
|
438
|
3 |
|
} |
|
439
|
|
|
|
|
440
|
|
|
/** |
|
441
|
|
|
* Updates BBC img tags in a message so that the width / height respect the forum settings. |
|
442
|
|
|
* |
|
443
|
|
|
* - Will add the width/height attrib if needed, or update existing ones if they break the rules |
|
444
|
|
|
*/ |
|
445
|
|
|
public function resizeBBCImages() |
|
446
|
|
|
{ |
|
447
|
|
|
global $modSettings; |
|
448
|
|
|
|
|
449
|
|
|
// We'll need this for image processing |
|
450
|
|
|
require_once(SUBSDIR . '/Attachments.subs.php'); |
|
451
|
|
|
|
|
452
|
|
|
// Find all the img tags - with or without width and height. |
|
453
|
|
|
preg_match_all('~\[img(\s+width=\d+)?(\s+height=\d+)?(\s+width=\d+)?\](.+?)\[/img\]~is', $this->message, $matches, PREG_PATTERN_ORDER); |
|
454
|
|
|
|
|
455
|
|
|
$replaces = array(); |
|
456
|
|
|
foreach ($matches[0] as $match => $dummy) |
|
457
|
|
|
{ |
|
458
|
|
|
// If the width was after the height, handle it. |
|
459
|
|
|
$matches[1][$match] = !empty($matches[3][$match]) ? $matches[3][$match] : $matches[1][$match]; |
|
460
|
|
|
|
|
461
|
|
|
// Now figure out if they had a desired height or width... |
|
462
|
|
|
$desired_width = !empty($matches[1][$match]) ? (int) substr(trim($matches[1][$match]), 6) : 0; |
|
463
|
|
|
$desired_height = !empty($matches[2][$match]) ? (int) substr(trim($matches[2][$match]), 7) : 0; |
|
464
|
|
|
|
|
465
|
|
|
// One was omitted, or both. We'll have to find its real size... |
|
466
|
|
|
if (empty($desired_width) || empty($desired_height)) |
|
467
|
|
|
{ |
|
468
|
|
|
list ($width, $height) = url_image_size(un_htmlspecialchars($matches[4][$match])); |
|
469
|
|
|
|
|
470
|
|
|
// They don't have any desired width or height! |
|
471
|
|
|
if (empty($desired_width) && empty($desired_height)) |
|
472
|
|
|
{ |
|
473
|
|
|
$desired_width = $width; |
|
474
|
|
|
$desired_height = $height; |
|
475
|
|
|
} |
|
476
|
|
|
// Scale it to the width... |
|
477
|
|
|
elseif (empty($desired_width) && !empty($height)) |
|
478
|
|
|
{ |
|
479
|
|
|
$desired_width = (int) (($desired_height * $width) / $height); |
|
480
|
|
|
} |
|
481
|
|
|
// Scale if to the height. |
|
482
|
|
|
elseif (!empty($width)) |
|
483
|
|
|
{ |
|
484
|
|
|
$desired_height = (int) (($desired_width * $height) / $width); |
|
485
|
|
|
} |
|
486
|
|
|
} |
|
487
|
|
|
|
|
488
|
|
|
// If the width and height are fine, just continue along... |
|
489
|
|
|
if ($desired_width <= $modSettings['max_image_width'] && $desired_height <= $modSettings['max_image_height']) |
|
490
|
|
|
{ |
|
491
|
|
|
continue; |
|
492
|
|
|
} |
|
493
|
|
|
|
|
494
|
|
|
// Too bad, it's too wide. Make it as wide as the maximum. |
|
495
|
|
View Code Duplication |
if ($desired_width > $modSettings['max_image_width'] && !empty($modSettings['max_image_width'])) |
|
496
|
|
|
{ |
|
497
|
|
|
$desired_height = (int) (($modSettings['max_image_width'] * $desired_height) / $desired_width); |
|
498
|
|
|
$desired_width = $modSettings['max_image_width']; |
|
499
|
|
|
} |
|
500
|
|
|
|
|
501
|
|
|
// Now check the height, as well. Might have to scale twice, even... |
|
502
|
|
View Code Duplication |
if ($desired_height > $modSettings['max_image_height'] && !empty($modSettings['max_image_height'])) |
|
503
|
|
|
{ |
|
504
|
|
|
$desired_width = (int) (($modSettings['max_image_height'] * $desired_width) / $desired_height); |
|
505
|
|
|
$desired_height = $modSettings['max_image_height']; |
|
506
|
|
|
} |
|
507
|
|
|
|
|
508
|
|
|
$replaces[$matches[0][$match]] = '[img' . (!empty($desired_width) ? ' width=' . $desired_width : '') . (!empty($desired_height) ? ' height=' . $desired_height : '') . ']' . $matches[4][$match] . '[/img]'; |
|
509
|
|
|
} |
|
510
|
|
|
|
|
511
|
|
|
// If any img tags were actually changed... |
|
512
|
|
|
if (!empty($replaces)) |
|
513
|
|
|
{ |
|
514
|
|
|
$this->message = strtr($this->message, $replaces); |
|
515
|
|
|
} |
|
516
|
|
|
} |
|
517
|
|
|
|
|
518
|
|
|
/** |
|
519
|
|
|
* Replace /me with the users name, including inside footnotes |
|
520
|
|
|
*/ |
|
521
|
3 |
|
private function _itsAllAbout() |
|
522
|
|
|
{ |
|
523
|
3 |
|
global $user_info; |
|
524
|
|
|
|
|
525
|
3 |
|
$me_regex = '~(\A|\n)/me(?: | )([^\n]*)(?:\z)?~i'; |
|
526
|
3 |
|
$footnote_regex = '~(\[footnote\])/me(?: | )([^\n]*?)(\[\/footnote\])~i'; |
|
527
|
|
|
|
|
528
|
3 |
|
if (preg_match('~[\[\]\\"]~', $user_info['name']) !== false) |
|
529
|
|
|
{ |
|
530
|
3 |
|
$this->message = preg_replace($me_regex, '$1[me="' . $user_info['name'] . '"]$2[/me]', $this->message); |
|
531
|
3 |
|
$this->message = preg_replace($footnote_regex, '$1[me="' . $user_info['name'] . '"]$2[/me]$3', $this->message); |
|
532
|
|
|
} |
|
533
|
|
|
else |
|
534
|
|
|
{ |
|
535
|
|
|
$this->message = preg_replace($me_regex, '$1[me=' . $user_info['name'] . ']$2[/me]', $this->message); |
|
536
|
|
|
$this->message = preg_replace($footnote_regex, '$1[me=' . $user_info['name'] . ']$2[/me]$3', $this->message); |
|
537
|
|
|
} |
|
538
|
3 |
|
} |
|
539
|
|
|
|
|
540
|
|
|
/** |
|
541
|
|
|
* Make sure lists have open and close tags |
|
542
|
|
|
*/ |
|
543
|
3 |
|
private function _validateLists() |
|
544
|
|
|
{ |
|
545
|
3 |
|
$list_open = substr_count($this->message, '[list]') + substr_count($this->message, '[list '); |
|
546
|
3 |
|
$list_close = substr_count($this->message, '[/list]'); |
|
547
|
|
|
|
|
548
|
3 |
View Code Duplication |
if ($list_close - $list_open > 0) |
|
549
|
|
|
{ |
|
550
|
|
|
$this->message = str_repeat('[list]', $list_close - $list_open) . $this->message; |
|
551
|
|
|
} |
|
552
|
|
|
|
|
553
|
3 |
View Code Duplication |
if ($list_open - $list_close > 0) |
|
554
|
|
|
{ |
|
555
|
|
|
$this->message = $this->message . str_repeat('[/list]', $list_open - $list_close); |
|
556
|
|
|
} |
|
557
|
3 |
|
} |
|
558
|
|
|
|
|
559
|
|
|
/** |
|
560
|
|
|
* Repair a few *cough* common mistakes from user input and from wizzy cut/paste |
|
561
|
|
|
*/ |
|
562
|
3 |
|
private function _fixMistakes() |
|
563
|
|
|
{ |
|
564
|
|
|
$mistake_fixes = array( |
|
565
|
|
|
// Find [table]s not followed by [tr]. |
|
566
|
3 |
|
'~\[table\](?![\s' . self::NBS . ']*\[tr\])~su' => '[table][tr]', |
|
567
|
|
|
// Find [tr]s not followed by [td] or [th] |
|
568
|
3 |
|
'~\[tr\](?![\s' . self::NBS . ']*\[t[dh]\])~su' => '[tr][td]', |
|
569
|
|
|
// Find [/td] and [/th]s not followed by something valid. |
|
570
|
3 |
|
'~\[/t([dh])\](?![\s' . self::NBS . ']*(?:\[t[dh]\]|\[/tr\]|\[/table\]))~su' => '[/t$1][/tr]', |
|
571
|
|
|
// Find [/tr]s not followed by something valid. |
|
572
|
3 |
|
'~\[/tr\](?![\s' . self::NBS . ']*(?:\[tr\]|\[/table\]))~su' => '[/tr][/table]', |
|
573
|
|
|
// Find [/td] [/th]s incorrectly followed by [/table]. |
|
574
|
3 |
|
'~\[/t([dh])\][\s' . self::NBS . ']*\[/table\]~su' => '[/t$1][/tr][/table]', |
|
575
|
|
|
// Find [table]s, [tr]s, and [/td]s (possibly correctly) followed by [td]. |
|
576
|
3 |
|
'~\[(table|tr|/td)\]([\s' . self::NBS . ']*)\[td\]~su' => '[$1]$2[_td_]', |
|
577
|
|
|
// Now, any [td]s left should have a [tr] before them. |
|
578
|
3 |
|
'~\[td\]~s' => '[tr][td]', |
|
579
|
|
|
// Look for [tr]s which are correctly placed. |
|
580
|
3 |
|
'~\[(table|/tr)\]([\s' . self::NBS . ']*)\[tr\]~su' => '[$1]$2[_tr_]', |
|
581
|
|
|
// Any remaining [tr]s should have a [table] before them. |
|
582
|
3 |
|
'~\[tr\]~s' => '[table][tr]', |
|
583
|
|
|
// Look for [/td]s or [/th]s followed by [/tr]. |
|
584
|
3 |
|
'~\[/t([dh])\]([\s' . self::NBS . ']*)\[/tr\]~su' => '[/t$1]$2[_/tr_]', |
|
585
|
|
|
// Any remaining [/tr]s should have a [/td]. |
|
586
|
3 |
|
'~\[/tr\]~s' => '[/td][/tr]', |
|
587
|
|
|
// Look for properly opened [li]s which aren't closed. |
|
588
|
3 |
|
'~\[li\]([^\[\]]+?)\[li\]~s' => '[li]$1[_/li_][_li_]', |
|
589
|
3 |
|
'~\[li\]([^\[\]]+?)\[/list\]~s' => '[_li_]$1[_/li_][/list]', |
|
590
|
3 |
|
'~\[li\]([^\[\]]+?)$~s' => '[li]$1[/li]', |
|
591
|
|
|
// Lists - find correctly closed items/lists. |
|
592
|
3 |
|
'~\[/li\]([\s' . self::NBS . ']*)\[/list\]~su' => '[_/li_]$1[/list]', |
|
593
|
|
|
// Find list items closed and then opened. |
|
594
|
3 |
|
'~\[/li\]([\s' . self::NBS . ']*)\[li\]~su' => '[_/li_]$1[_li_]', |
|
595
|
|
|
// Now, find any [list]s or [/li]s followed by [li]. |
|
596
|
3 |
|
'~\[(list(?: [^\]]*?)?|/li)\]([\s' . self::NBS . ']*)\[li\]~su' => '[$1]$2[_li_]', |
|
597
|
|
|
// Allow for sub lists. |
|
598
|
3 |
|
'~\[/li\]([\s' . self::NBS . ']*)\[list\]~u' => '[_/li_]$1[list]', |
|
599
|
3 |
|
'~\[/list\]([\s' . self::NBS . ']*)\[li\]~u' => '[/list]$1[_li_]', |
|
600
|
|
|
// Any remaining [li]s weren't inside a [list]. |
|
601
|
3 |
|
'~\[li\]~' => '[list][li]', |
|
602
|
|
|
// Any remaining [/li]s weren't before a [/list]. |
|
603
|
3 |
|
'~\[/li\]~' => '[/li][/list]', |
|
604
|
|
|
// Put the correct ones back how we found them. |
|
605
|
3 |
|
'~\[_(li|/li|td|tr|/tr)_\]~' => '[$1]', |
|
606
|
|
|
// Images with no real url. |
|
607
|
3 |
|
'~\[img\]https?://.{0,7}\[/img\]~' => '', |
|
608
|
|
|
); |
|
609
|
|
|
|
|
610
|
|
|
// Fix up some use of tables without [tr]s, etc. (it has to be done more than once to catch it all.) |
|
611
|
3 |
|
for ($j = 0; $j < 3; $j++) |
|
612
|
|
|
{ |
|
613
|
3 |
|
$this->message = preg_replace(array_keys($mistake_fixes), $mistake_fixes, $this->message); |
|
614
|
|
|
} |
|
615
|
3 |
|
} |
|
616
|
|
|
|
|
617
|
|
|
/** |
|
618
|
|
|
* Replace our token-ized message with the saved code blocks |
|
619
|
|
|
*/ |
|
620
|
3 |
|
private function _restoreCodeBlocks() |
|
621
|
|
|
{ |
|
622
|
3 |
|
if (!empty($this->code_blocks)) |
|
623
|
|
|
{ |
|
624
|
2 |
|
$this->message = str_replace(array_keys($this->code_blocks), array_values($this->code_blocks), $this->message); |
|
625
|
|
|
} |
|
626
|
3 |
|
} |
|
627
|
|
|
|
|
628
|
|
|
/** |
|
629
|
|
|
* Validates and corrects table structure |
|
630
|
|
|
* |
|
631
|
|
|
* What it does |
|
632
|
|
|
* - Checks tables for correct tag order / nesting |
|
633
|
|
|
* - Adds in missing closing tags, removes excess closing tags |
|
634
|
|
|
* - Although it prevents markup error, it can mess-up the intended (abiet wrong) layout |
|
635
|
|
|
* driving the post author in to a furious rage |
|
636
|
|
|
* |
|
637
|
|
|
*/ |
|
638
|
3 |
|
private function _preparseTable() |
|
639
|
|
|
{ |
|
640
|
3 |
|
$table_check = $this->message; |
|
641
|
3 |
|
$table_offset = 0; |
|
642
|
3 |
|
$table_array = array(); |
|
643
|
|
|
|
|
644
|
|
|
// Define the allowable tags after a give tag |
|
645
|
|
|
$table_order = array( |
|
646
|
3 |
|
'table' => array('tr'), |
|
647
|
|
|
'tr' => array('td', 'th'), |
|
648
|
|
|
'td' => array('table'), |
|
649
|
|
|
'th' => array(''), |
|
650
|
|
|
); |
|
651
|
|
|
|
|
652
|
|
|
// Find all closing tags (/table /tr /td etc) |
|
653
|
3 |
|
while (preg_match('~\[(/)*(table|tr|td|th)\]~', $table_check, $matches) === 1) |
|
654
|
|
|
{ |
|
655
|
|
|
// Keep track of where this is. |
|
656
|
1 |
|
$offset = strpos($table_check, $matches[0]); |
|
657
|
1 |
|
$remove_tag = false; |
|
658
|
|
|
|
|
659
|
|
|
// Is it opening? |
|
660
|
1 |
|
if ($matches[1] != '/') |
|
661
|
|
|
{ |
|
662
|
|
|
// If the previous table tag isn't correct simply remove it. |
|
663
|
1 |
|
if ((!empty($table_array) && !in_array($matches[2], $table_order[$table_array[0]])) || (empty($table_array) && $matches[2] !== 'table')) |
|
664
|
|
|
{ |
|
665
|
|
|
$remove_tag = true; |
|
666
|
|
|
} |
|
667
|
|
|
// Record this was the last tag. |
|
668
|
|
|
else |
|
669
|
|
|
{ |
|
670
|
1 |
|
array_unshift($table_array, $matches[2]); |
|
671
|
|
|
} |
|
672
|
|
|
} |
|
673
|
|
|
// Otherwise is closed! |
|
674
|
|
|
else |
|
675
|
|
|
{ |
|
676
|
|
|
// Only keep the tag if it's closing the right thing. |
|
677
|
1 |
|
if (empty($table_array) || ($table_array[0] != $matches[2])) |
|
678
|
|
|
{ |
|
679
|
|
|
$remove_tag = true; |
|
680
|
|
|
} |
|
681
|
|
|
else |
|
682
|
|
|
{ |
|
683
|
1 |
|
array_shift($table_array); |
|
684
|
|
|
} |
|
685
|
|
|
} |
|
686
|
|
|
|
|
687
|
|
|
// Removing? |
|
688
|
1 |
|
if ($remove_tag) |
|
689
|
|
|
{ |
|
690
|
|
|
$this->message = substr($this->message, 0, $table_offset + $offset) . substr($this->message, $table_offset + strlen($matches[0]) + $offset); |
|
691
|
|
|
|
|
692
|
|
|
// We've lost some data. |
|
693
|
|
|
$table_offset -= strlen($matches[0]); |
|
694
|
|
|
} |
|
695
|
|
|
|
|
696
|
|
|
// Remove everything up to here. |
|
697
|
1 |
|
$table_offset += $offset + strlen($matches[0]); |
|
698
|
1 |
|
$table_check = substr($table_check, $offset + strlen($matches[0])); |
|
699
|
|
|
} |
|
700
|
|
|
|
|
701
|
|
|
// Close any remaining table tags. |
|
702
|
3 |
|
foreach ($table_array as $tag) |
|
703
|
|
|
{ |
|
704
|
|
|
$this->message .= '[/' . $tag . ']'; |
|
705
|
|
|
} |
|
706
|
3 |
|
} |
|
707
|
|
|
|
|
708
|
|
|
/** |
|
709
|
|
|
* This is very simple, and just removes things done by preparsecode. |
|
710
|
|
|
* |
|
711
|
|
|
* @param string $message |
|
712
|
|
|
*/ |
|
713
|
|
|
public function un_preparsecode($message) |
|
714
|
|
|
{ |
|
715
|
|
|
// Protect CODE blocks from further processing |
|
716
|
|
|
$this->message = $message; |
|
717
|
|
|
$this->_tokenizeCodeBlocks(); |
|
718
|
|
|
|
|
719
|
|
|
// Pass integration the tokenized message and array |
|
720
|
|
|
call_integration_hook('integrate_unpreparse_code', array(&$this->message, &$this->code_blocks, 0)); |
|
721
|
|
|
|
|
722
|
|
|
// Restore the code blocks |
|
723
|
|
|
$this->_restoreCodeBlocks(); |
|
724
|
|
|
|
|
725
|
|
|
// Change breaks back to \n's and &nsbp; back to spaces. |
|
726
|
|
|
return preg_replace('~<br( /)?' . '>~', "\n", str_replace(' ', ' ', $this->message)); |
|
727
|
|
|
} |
|
728
|
|
|
|
|
729
|
|
|
/** |
|
730
|
|
|
* Ensure tags inside of nobbc do not get parsed by converting the markers to html entities |
|
731
|
|
|
* |
|
732
|
|
|
* @param string[] $matches |
|
733
|
|
|
*/ |
|
734
|
|
|
private function _preparsecode_nobbc_callback($matches) |
|
735
|
|
|
{ |
|
736
|
|
|
return '[nobbc]' . strtr($matches[1], array('[' => '[', ']' => ']', ':' => ':', '@' => '@')) . '[/nobbc]'; |
|
737
|
|
|
} |
|
738
|
|
|
|
|
739
|
|
|
/** |
|
740
|
|
|
* Use only the primary (first) font face when multiple are supplied |
|
741
|
|
|
* |
|
742
|
|
|
* @param string[] $matches |
|
743
|
|
|
*/ |
|
744
|
2 |
|
private function _preparsecode_font_callback($matches) |
|
745
|
|
|
{ |
|
746
|
2 |
|
$fonts = explode(',', $matches[1]); |
|
747
|
2 |
|
$font = trim(un_htmlspecialchars($fonts[0]), ' "\''); |
|
748
|
|
|
|
|
749
|
2 |
|
return '[font=' . $font . ']' . $matches[2]; |
|
750
|
|
|
} |
|
751
|
|
|
|
|
752
|
|
|
/** |
|
753
|
|
|
* Takes a tag and changes it to lowercase |
|
754
|
|
|
* |
|
755
|
|
|
* @param string[] $matches |
|
756
|
|
|
*/ |
|
757
|
2 |
|
private function _preparsecode_lowertags_callback($matches) |
|
758
|
|
|
{ |
|
759
|
2 |
|
return '[' . $matches[1] . strtolower($matches[2]) . $matches[3] . ']'; |
|
760
|
|
|
} |
|
761
|
|
|
|
|
762
|
|
|
/** |
|
763
|
|
|
* Ensure image tags do not load anything by themselves (security) |
|
764
|
|
|
* |
|
765
|
|
|
* @param string[] $matches |
|
766
|
|
|
*/ |
|
767
|
|
|
private function _fixTags_img_callback($matches) |
|
768
|
|
|
{ |
|
769
|
|
|
return $matches[1] . preg_replace('~action(=|%3d)(?!dlattach)~i', 'action-', $matches[2]) . '[/img]'; |
|
770
|
|
|
} |
|
771
|
|
|
|
|
772
|
|
|
/** |
|
773
|
|
|
* Find and return PreparseCode instance if it exists, |
|
774
|
|
|
* or create a new instance |
|
775
|
|
|
* |
|
776
|
|
|
* @return PreparseCode |
|
777
|
|
|
*/ |
|
778
|
2 |
|
public static function instance() |
|
779
|
|
|
{ |
|
780
|
2 |
|
if (self::$instance === null) |
|
781
|
|
|
{ |
|
782
|
1 |
|
self::$instance = new PreparseCode; |
|
783
|
|
|
} |
|
784
|
|
|
|
|
785
|
2 |
|
return self::$instance; |
|
786
|
|
|
} |
|
787
|
|
|
} |
|
788
|
|
|
|