2 require_once('common/include/Error.class');
5 // length of the bbcode bbcode_uid that will be inserted in each bbcode tag
6 define("BBCODE_UID_LEN", 10);
8 // Need to initialize the random numbers only ONCE
9 mt_srand( (double) microtime() * 1000000);
12 class BBCodeSupport extends Error {
14 var $bbcode_tpl = array();
17 function BBCodeSupport(){
20 // some inner stuff to initialize
21 $this->bbcodeStyleInitialize();
25 function getTextReadyForDisplay($text, $bbcode_uid){
26 $tmp = str_replace("\n", "\n<br />\n", $text);
27 $tmp = $this->displayText($tmp,$bbcode_uid);
28 $tmp = $this->makeClickable($tmp);
29 //$tmp = $this->smiliesPass($tmp);
37 * bbcodeStyleInitialize() -
39 * Initialize all the bbcode tags and how they should be displayed
43 function bbcodeStyleInitialize(){
44 $this->bbcode_tpl = array('b_open' => '<span style="font-weight:bold">',
45 'b_close' => '</span>',
46 'i_open' => '<span style="font-style:italic">',
47 'i_close' => '</span>',
48 'u_open' => '<span style="text-decoration:underline">',
49 'u_close' => '</span>',
50 'color_open' => '<span style="color: \\1">',
51 'color_close' => '</span>',
52 'size_open' => '<span style="font-size: \\1px; line-height: normal">',
53 'size_close' => '</span>',
55 'img' => '<img src="\\1" border="0" />',
57 'ulist_open' => '<ul>',
58 'ulist_close' => '</ul>',
59 'olist_open' => '<ol type="\\1">',
60 'olist_close' => '</ol>',
63 'code_open' => '<table width="90%" cellspacing="1" cellpadding="3" border="0" align="center">
65 <td><span style="font-weight:bold"><b>{L_CODE}:</b></span></td>
69 'code_close' => ' </td>
74 'quote_open' => '<table width="90%" cellspacing="1" cellpadding="3" border="0" align="center">
76 <td><span style="font-weight:bold">{L_QUOTE}:</span></td>
80 'quote_close' => ' </td>
84 'quote_username_open' => '<table width="90%" cellspacing="1" cellpadding="3" border="0" align="center">
86 <td><b>\\1 {L_WROTE}:</b></td>
91 'email' => '<a href="mailto:\\1">\\1</A>',
93 'url1' => '<a href="\\1\\2" target="_blank">\\1\\2</a>',
94 'url2' => '<a href="http://\\1" target="_blank">\\1</a>',
95 'url3' => '<a href="\\1\\2" target="_blank">\\3</a>',
96 'url4' => '<a href="http://\\1" target="_blank">\\2</a>',
97 'urltask' => '<a href="/pm/task.php?func=detailtask&project_task_id=\\3&group_id=\\1&group_project_id=\\2">\\4</a>',
98 'urlartifact' => '<a href="/tracker/index.php?func=detail&aid=\\1">Artifact\\1</a>'
105 * Does second-pass bbencoding. This should be used before displaying the message in
106 * a thread. Assumes the message is already first-pass encoded, and we are given the
107 * correct UID as used in first-pass encoding.
109 function displayText($text, $bbcode_uid)
111 global $Language, $HTML;
113 // First: If there isn't a "[" and a "]" in the message, don't bother.
114 if (!(strpos($text, "[") && strpos($text, "]"))){
115 // IT PASS HERE WITH MY MAC SINCE GFORGE WAS WRITTEN WITH encoding="UTF-8"
116 // IF SOMEONE COULD TEST IT WITH PC TO SEE WHAT HAPPENS
120 // pad it with a space so we can distinguish between FALSE and matching the 1st char (index 0).
121 // This is important; bbencode_quote(), bbencode_list(), and bbencode_code() all depend on it.
124 // we translate the text (for quote and text)
125 $this->bbcode_tpl['quote_open'] = str_replace("{L_QUOTE}", _('Quote'), $this->bbcode_tpl['quote_open']);
126 $this->bbcode_tpl['code_open'] = str_replace("{L_CODE}", _('Code'), $this->bbcode_tpl['code_open']);
127 $this->bbcode_tpl['quote_username_open'] = str_replace("{L_WROTE}", _('Wrote'), $this->bbcode_tpl['quote_username_open']);
130 // [CODE] and [/CODE] for posting code (HTML, PHP, C etc etc) in your posts.
131 $text = $this->bbencodeSecondPassCode($text, $bbcode_uid, $bbcode_tpl);
133 // [list] and [list=x] for (un)ordered lists.
135 $text = str_replace("[list:$bbcode_uid]", $this->bbcode_tpl['ulist_open'], $text);
137 $text = str_replace("[*:$bbcode_uid]", $this->bbcode_tpl['listitem'], $text);
139 $text = str_replace("[/list:u:$bbcode_uid]", $this->bbcode_tpl['ulist_close'], $text);
140 $text = str_replace("[/list:o:$bbcode_uid]", $this->bbcode_tpl['olist_close'], $text);
142 $text = preg_replace("/\[list=([a1]):$bbcode_uid\]/si", $this->bbcode_tpl['olist_open'], $text);
145 $text = preg_replace("/\[color=(\#[0-9A-F]{6}|[a-z]+):$bbcode_uid\]/si", $this->bbcode_tpl['color_open'], $text);
146 $text = str_replace("[/color:$bbcode_uid]", $this->bbcode_tpl['color_close'], $text);
147 // if color set to default, then we get the default color of that theme
148 $text = str_replace("[/color:default", $HTML->FONT_CONTENT, $text);
152 $text = preg_replace("/\[size=([\-\+]?[1-2]?[0-9]):$bbcode_uid\]/si", $this->bbcode_tpl['size_open'], $text);
153 $text = str_replace("[/size:$bbcode_uid]", $this->bbcode_tpl['size_close'], $text);
155 // [QUOTE] and [/QUOTE] for posting replies with quote, or just for quoting stuff.
156 $text = str_replace("[quote:$bbcode_uid]", $this->bbcode_tpl['quote_open'], $text);
157 $text = str_replace("[/quote:$bbcode_uid]", $this->bbcode_tpl['quote_close'], $text);
159 // New one liner to deal with opening quotes with usernames...
160 // replaces the two line version that I had here before..
161 $text = preg_replace("/\[quote:$bbcode_uid=(?:\"?([^\"]*)\"?)\]/si", $this->bbcode_tpl['quote_username_open'], $text);
164 // [b] and [/b] for bolding text.
165 $text = str_replace("[b:$bbcode_uid]", $this->bbcode_tpl['b_open'], $text);
166 $text = str_replace("[/b:$bbcode_uid]", $this->bbcode_tpl['b_close'], $text);
168 // [u] and [/u] for underlining text.
169 $text = str_replace("[u:$bbcode_uid]", $this->bbcode_tpl['u_open'], $text);
170 $text = str_replace("[/u:$bbcode_uid]", $this->bbcode_tpl['u_close'], $text);
172 // [i] and [/i] for italicizing text.
173 $text = str_replace("[i:$bbcode_uid]", $this->bbcode_tpl['i_open'], $text);
174 $text = str_replace("[/i:$bbcode_uid]", $this->bbcode_tpl['i_close'], $text);
176 // Patterns and replacements for URL and email tags..
178 $replacements = array();
180 // [img]image_url_here[/img] code..
181 // This one gets first-passed..
182 $patterns[0] = "#\[img:$bbcode_uid\](.*?)\[/img:$bbcode_uid\]#si";
183 $replacements[0] = $this->bbcode_tpl['img'];
185 // [url]xxxx://www.phpbb.com[/url] code..
186 $patterns[1] = "#\[url\]([a-z]+?://){1}([a-z0-9\-\.,\?!%\*_\#:;~\\&$@\/=\+\(\)]+)\[/url\]#si";
187 $replacements[1] = $this->bbcode_tpl['url1'];
189 // [url]www.phpbb.com[/url] code.. (no xxxx:// prefix).
190 $patterns[2] = "#\[url\]([a-z0-9\-\.,\?!%\*_\#:;~\\&$@\/=\+\(\)]+)\[/url\]#si";
191 $replacements[2] = $this->bbcode_tpl['url2'];
193 // [url=xxxx://www.phpbb.com]phpBB[/url] code..
194 $patterns[3] = "#\[url=([a-z]+?://){1}([a-z0-9\-\.,\?!%\*_\#:;~\\&$@\/=\+\(\)]+)\](.*?)\[/url\]#si";
195 $replacements[3] = $this->bbcode_tpl['url3'];
197 // [url=www.phpbb.com]phpBB[/url] code.. (no xxxx:// prefix).
198 $patterns[4] = "#\[url=([a-z0-9\-\.,\?!%\*_\#:;~\\&$@\/=\+\(\)]+)\](.*?)\[/url\]#si";
199 $replacements[4] = $this->bbcode_tpl['url4'];
201 // [email]user@domain.tld[/email] code..
202 $patterns[5] = "#\[email\]([a-z0-9\-_.]+?@[\w\-]+\.([\w\-\.]+\.)?[\w]+)\[/email\]#si";
203 $replacements[5] = $this->bbcode_tpl['email'];
205 // [task]Forumid:Subproject:Taskid:TaskName[/task] code..
206 $patterns[6] = "#\[task\]([0-9]+?):([0-9]+?):([0-9]+?):([a-z0-9\-_.]+?)\[/task\]#si";
207 $replacements[6] = $this->bbcode_tpl['urltask'];
209 // [artifact]artifactID[/artifact] code..
210 $patterns[7] = "#\[artifact\]([0-9]+?)\[/artifact\]#si";
211 $replacements[7] = $this->bbcode_tpl['urlartifact'];
214 $text = preg_replace($patterns, $replacements, $text);
217 // Remove our padding from the string..
218 $text = substr($text, 1);
225 function makeBBCodeUID(){
226 // Unique ID for this message..
228 $bbcode_uid = md5(mt_rand());
229 $bbcode_uid = substr($bbcode_uid, 0, BBCODE_UID_LEN);
234 function prepareText($text, $bbcode_uid) {
235 // pad it with a space so we can distinguish between FALSE and matching the 1st char (index 0).
236 // This is important; bbencode_quote(), bbencode_list(), and bbencode_code() all depend on it.
239 // [CODE] and [/CODE] for posting code (HTML, PHP, C etc etc) in your posts.
240 $text = $this->bbencodeFirstPassPDA($text, $bbcode_uid, '[code]', '[/code]', '', true, '');
242 // [QUOTE] and [/QUOTE] for posting replies with quote, or just for quoting stuff.
243 $text = $this->bbencodeFirstPassPDA($text, $bbcode_uid, '[quote]', '[/quote]', '', false, '');
245 $text = $this->bbencodeFirstPassPDA($text, $bbcode_uid, '/\[quote=(\\\\"[^"]*?\\\\")\]/is', '[/quote]', '', false, '', "[quote:$bbcode_uid=\\1]");
247 // [list] and [list=x] for (un)ordered lists.
249 $open_tag[0] = "[list]";
252 $text = $this->bbencodeFirstPassPDA($text, $bbcode_uid, $open_tag, "[/list]", "[/list:u]", false, 'replace_list_items');
254 $open_tag[0] = "[list=1]";
255 $open_tag[1] = "[list=a]";
258 $text = $this->bbencodeFirstPassPDA($text, $bbcode_uid, $open_tag, "[/list]", "[/list:o]", false, 'replace_list_items');
260 // [color] and [/color] for setting text color
261 $text = preg_replace("#\[color=(\#[0-9A-F]{6}|[a-z\-]+)\](.*?)\[/color\]#si", "[color=\\1:$bbcode_uid]\\2[/color:$bbcode_uid]", $text);
263 // [size] and [/size] for setting text size
264 $text = preg_replace("#\[size=([\-\+]?[1-2]?[0-9])\](.*?)\[/size\]#si", "[size=\\1:$bbcode_uid]\\2[/size:$bbcode_uid]", $text);
266 // [b] and [/b] for bolding text.
267 $text = preg_replace("#\[b\](.*?)\[/b\]#si", "[b:$bbcode_uid]\\1[/b:$bbcode_uid]", $text);
269 // [u] and [/u] for underlining text.
270 $text = preg_replace("#\[u\](.*?)\[/u\]#si", "[u:$bbcode_uid]\\1[/u:$bbcode_uid]", $text);
272 // [i] and [/i] for italicizing text.
273 $text = preg_replace("#\[i\](.*?)\[/i\]#si", "[i:$bbcode_uid]\\1[/i:$bbcode_uid]", $text);
275 // [img]image_url_here[/img] code..
276 $text = preg_replace("#\[img\](http(s)?://)([a-z0-9\-\.,\?!%\*_\#:;~\\&$@\/=\+]+)\[/img\]#si", "[img:$bbcode_uid]\\1\\3[/img:$bbcode_uid]", $text);
278 // Remove our padding from the string..
279 $text = substr($text, 1);
287 * $text - The text to operate on.
288 * $bbcode_uid - The UID to add to matching tags.
289 * $open_tag - The opening tag to match. Can be an array of opening tags.
290 * $close_tag - The closing tag to match.
291 * $close_tag_new - The closing tag to replace with.
292 * $mark_lowest_level - boolean - should we specially mark the tags that occur
293 * at the lowest level of nesting? (useful for [code], because
294 * we need to match these tags first and transform HTML tags
295 * in their contents..
296 * $func - This variable should contain a string that is the name of a function.
297 * That function will be called when a match is found, and passed 2
298 * parameters: ($text, $bbcode_uid). The function should return a string.
299 * This is used when some transformation needs to be applied to the
300 * text INSIDE a pair of matching tags. If this variable is FALSE or the
301 * empty string, it will not be executed.
302 * If open_tag is an array, then the pda will try to match pairs consisting of
303 * any element of open_tag followed by close_tag. This allows us to match things
304 * like [list=A]...[/list] and [list=1]...[/list] in one pass of the PDA.
306 * NOTES: - this function assumes the first character of $text is a space.
307 * - every opening tag and closing tag must be of the [...] format.
309 function bbencodeFirstPassPDA($text, $bbcode_uid, $open_tag, $close_tag, $close_tag_new, $mark_lowest_level, $func, $open_regexp_replace = false){
312 if (!$close_tag_new || ($close_tag_new == ''))
314 $close_tag_new = $close_tag;
317 $close_tag_length = strlen($close_tag);
318 $close_tag_new_length = strlen($close_tag_new);
319 $uid_length = strlen($bbcode_uid);
321 $use_function_pointer = ($func && ($func != ''));
325 if (is_array($open_tag)) {
326 if (0 == count($open_tag)) {
327 // No opening tags to match, so return.
330 $open_tag_count = count($open_tag);
333 // only one opening tag. make it into a 1-element array.
334 $open_tag_temp = $open_tag;
336 $open_tag[0] = $open_tag_temp;
340 $open_is_regexp = false;
342 if ($open_regexp_replace) {
343 $open_is_regexp = true;
344 if (!is_array($open_regexp_replace)) {
345 $open_regexp_temp = $open_regexp_replace;
346 $open_regexp_replace = array();
347 $open_regexp_replace[0] = $open_regexp_temp;
351 if ($mark_lowest_level && $open_is_regexp) {
352 $this->setError("Unsupported operation for bbcode_first_pass_pda().");
356 // Start at the 2nd char of the string, looking for opening tags.
358 while ($curr_pos && ($curr_pos < strlen($text))) {
359 $curr_pos = strpos($text, "[", $curr_pos);
361 // If not found, $curr_pos will be 0, and the loop will end.
363 // We found a [. It starts at $curr_pos.
364 // check if it's a starting or ending tag.
365 $found_start = false;
366 $which_start_tag = "";
367 $start_tag_index = -1;
368 for ($i = 0; $i < $open_tag_count; $i++) {
369 // Grab everything until the first "]"...
370 $possible_start = substr($text, $curr_pos, strpos($text, "]", $curr_pos + 1) - $curr_pos + 1);
373 // We're going to try and catch usernames with "[' characters.
375 if( preg_match('/\[quote\=\\\\"/si', $possible_start) && !preg_match('/\[quote=\\\\"[^"]*\\\\"\]/si', $possible_start) ) {
377 // OK we are in a quote tag that probably contains a ] bracket.
378 // Grab a bit more of the string to hopefully get all of it..
380 $possible_start = substr($text, $curr_pos, strpos($text, "\"]", $curr_pos + 1) - $curr_pos + 2);
383 // Now compare, either using regexp or not.
385 if ($open_is_regexp) {
386 $match_result = array();
387 // PREG regexp comparison.
388 if (preg_match($open_tag[$i], $possible_start, $match_result)) {
390 $which_start_tag = $match_result[0];
391 $start_tag_index = $i;
396 // straightforward string comparison.
397 if (0 == strcasecmp($open_tag[$i], $possible_start)){
399 $which_start_tag = $open_tag[$i];
400 $start_tag_index = $i;
407 // We have an opening tag.
408 // Push its position, the text we matched, and its index in the open_tag array on to the stack, and then keep going to the right.
409 $match = array("pos" => $curr_pos, "tag" => $which_start_tag, "index" => $start_tag_index);
410 $this->bbcodeArrayPush($stack, $match);
412 // Rather than just increment $curr_pos
413 // Set it to the ending of the tag we just found
414 // Keeps error in nested tag from breaking out
415 // of table structure..
417 $curr_pos = $curr_pos + strlen($possible_start);
421 // check for a closing tag..
422 $possible_end = substr($text, $curr_pos, $close_tag_length);
423 if (0 == strcasecmp($close_tag, $possible_end)){
424 // We have an ending tag.
425 // Check if we've already found a matching starting tag.
426 if (sizeof($stack) > 0) {
427 // There exists a starting tag.
428 $curr_nesting_depth = sizeof($stack);
429 // We need to do 2 replacements now.
430 $match = $this->bbcodeArrayPop($stack);
431 $start_index = $match['pos'];
432 $start_tag = $match['tag'];
433 $start_length = strlen($start_tag);
434 $start_tag_index = $match['index'];
438 $start_tag = preg_replace($open_tag[$start_tag_index], $open_regexp_replace[$start_tag_index], $start_tag);
441 // everything before the opening tag.
442 $before_start_tag = substr($text, 0, $start_index);
444 // everything after the opening tag, but before the closing tag.
445 $between_tags = substr($text, $start_index + $start_length, $curr_pos - $start_index - $start_length);
447 // Run the given function on the text between the tags..
448 if ($use_function_pointer)
450 $between_tags = $func($between_tags, $bbcode_uid);
453 // everything after the closing tag.
454 $after_end_tag = substr($text, $curr_pos + $close_tag_length);
456 // Mark the lowest nesting level if needed.
457 if ($mark_lowest_level && ($curr_nesting_depth == 1))
459 if ($open_tag[0] == '[code]')
461 $code_entities_match = array('#<#', '#>#', '#"#', '#:#', '#\[#', '#\]#', '#\(#', '#\)#', '#\{#', '#\}#');
462 $code_entities_replace = array('<', '>', '"', ':', '[', ']', '(', ')', '{', '}');
463 $between_tags = preg_replace($code_entities_match, $code_entities_replace, $between_tags);
465 $text = $before_start_tag . substr($start_tag, 0, $start_length - 1) . ":$curr_nesting_depth:$bbcode_uid]";
466 $text .= $between_tags . substr($close_tag_new, 0, $close_tag_new_length - 1) . ":$curr_nesting_depth:$bbcode_uid]";
470 if ($open_tag[0] == '[code]')
472 $text = $before_start_tag . '[code]';
473 $text .= $between_tags . '[/code]';
479 $text = $before_start_tag . $start_tag;
483 $text = $before_start_tag . substr($start_tag, 0, $start_length - 1) . ":$bbcode_uid]";
485 $text .= $between_tags . substr($close_tag_new, 0, $close_tag_new_length - 1) . ":$bbcode_uid]";
489 $text .= $after_end_tag;
491 // Now.. we've screwed up the indices by changing the length of the string.
492 // So, if there's anything in the stack, we want to resume searching just after it.
493 // otherwise, we go back to the start.
494 if (sizeof($stack) > 0){
495 $match = bbcodeArrayPop($stack);
496 $curr_pos = $match['pos'];
497 bbcodeArrayPush($stack, $match);
505 // No matching start tag found. Increment pos, keep going.
510 // No starting tag or ending tag.. Increment pos, keep looping.,
522 * Does second-pass bbencoding of the [code] tags. This includes
523 * running htmlspecialchars() over the text contained between
524 * any pair of [code] tags that are at the first level of
525 * nesting. Tags at the first level of nesting are indicated
526 * by this format: [code:1:$bbcode_uid] ... [/code:1:$bbcode_uid]
527 * Other tags are in this format: [code:$bbcode_uid] ... [/code:$bbcode_uid]
529 function bbencodeSecondPassCode($text, $bbcode_uid) {
532 $code_start_html = $this->bbcode_tpl['code_open'];
533 $code_end_html = $this->bbcode_tpl['code_close'];
535 // First, do all the 1st-level matches. These need an htmlspecialchars() run,
536 // so they have to be handled differently.
537 $match_count = preg_match_all("#\[code:1:$bbcode_uid\](.*?)\[/code:1:$bbcode_uid\]#si", $text, $matches);
539 for ($i = 0; $i < $match_count; $i++){
540 $before_replace = $matches[1][$i];
541 $after_replace = $matches[1][$i];
543 // Replace 2 spaces with " " so non-tabbed code indents without making huge long lines.
544 $after_replace = str_replace(" ", " ", $after_replace);
545 // now Replace 2 spaces with " " to catch odd #s of spaces.
546 $after_replace = str_replace(" ", " ", $after_replace);
548 // Replace tabs with " " so tabbed code indents sorta right without making huge long lines.
549 $after_replace = str_replace("\t", " ", $after_replace);
551 $str_to_match = "[code:1:$bbcode_uid]" . $before_replace . "[/code:1:$bbcode_uid]";
553 $replacement = $code_start_html;
554 $replacement .= $after_replace;
555 $replacement .= $code_end_html;
557 $text = str_replace($str_to_match, $replacement, $text);
560 // Now, do all the non-first-level matches. These are simple.
561 $text = str_replace("[code:$bbcode_uid]", $code_start_html, $text);
562 $text = str_replace("[/code:$bbcode_uid]", $code_end_html, $text);
571 * Escapes the "/" character with "\/". This is useful when you need
572 * to stick a runtime string into a PREG regexp that is being delimited
575 function escapeSlashes($input){
576 $output = str_replace('/', '\/', $input);
581 * This function does exactly what the PHP4 function array_push() does
582 * however, to keep phpBB compatable with PHP 3 we had to come up with our own
583 * method of doing it.
585 function bbcodeArrayPush(&$stack, $value){
587 return(sizeof($stack));
591 * This function does exactly what the PHP4 function array_pop() does
592 * however, to keep phpBB compatable with PHP 3 we had to come up with our own
593 * method of doing it.
595 function bbcodeArrayPop(&$stack){
596 $arrSize = count($stack);
599 while(list($key, $val) = each($stack)){
600 if($x < count($stack))
618 * I KNOW THIS IS UGLY, BUT I HAVEN'T FOUND A WAY TO PUT THAT FONCTION IN THE CLASS AND
619 * CALL IT AS A VARIABLE FUNCTION
620 * This is used to change a [*] tag into a [*:$bbcode_uid] tag as part
621 * of the first-pass bbencoding of [list] tags. It fits the
622 * standard required in order to be passed as a variable
623 * function into $this->bbencodeFirstPassPDA().
625 function replace_list_items($text, $bbcode_uid)
627 $text = str_replace("[*]", "[*:$bbcode_uid]", $text);