001
014
015 package com.liferay.portal.parsers.bbcode;
016
017 import com.liferay.portal.kernel.log.Log;
018 import com.liferay.portal.kernel.log.LogFactoryUtil;
019 import com.liferay.portal.kernel.parsers.bbcode.BBCodeTranslator;
020 import com.liferay.portal.kernel.security.pacl.DoPrivileged;
021 import com.liferay.portal.kernel.util.GetterUtil;
022 import com.liferay.portal.kernel.util.HtmlUtil;
023 import com.liferay.portal.kernel.util.IntegerWrapper;
024 import com.liferay.portal.kernel.util.StringBundler;
025 import com.liferay.portal.kernel.util.StringPool;
026 import com.liferay.portal.kernel.util.StringUtil;
027 import com.liferay.portal.kernel.util.Validator;
028
029 import java.util.Collection;
030 import java.util.HashMap;
031 import java.util.List;
032 import java.util.Map;
033 import java.util.Stack;
034 import java.util.regex.Matcher;
035 import java.util.regex.Pattern;
036
037
040 @DoPrivileged
041 public class HtmlBBCodeTranslatorImpl implements BBCodeTranslator {
042
043 public HtmlBBCodeTranslatorImpl() {
044 _listStyles = new HashMap<String, String>();
045
046 _listStyles.put("a", "list-style: lower-alpha outside;");
047 _listStyles.put("A", "list-style: upper-alpha outside;");
048 _listStyles.put("1", "list-style: decimal outside;");
049 _listStyles.put("i", "list-style: lower-roman outside;");
050 _listStyles.put("I", "list-style: upper-roman outside;");
051
052 _excludeNewLineTypes = new HashMap<String, Integer>();
053
054 _excludeNewLineTypes.put("*", BBCodeParser.TYPE_TAG_START_END);
055 _excludeNewLineTypes.put("li", BBCodeParser.TYPE_TAG_START_END);
056 _excludeNewLineTypes.put("table", BBCodeParser.TYPE_TAG_END);
057 _excludeNewLineTypes.put("td", BBCodeParser.TYPE_TAG_START_END);
058 _excludeNewLineTypes.put("th", BBCodeParser.TYPE_TAG_START_END);
059 _excludeNewLineTypes.put("tr", BBCodeParser.TYPE_TAG_START_END);
060
061 _bbCodeCharacters = new HashMap<String, String>();
062
063 _bbCodeCharacters.put("&", "&");
064 _bbCodeCharacters.put("<", "<");
065 _bbCodeCharacters.put(">", ">");
066 _bbCodeCharacters.put("\"", """);
067 _bbCodeCharacters.put("'", "'");
068 _bbCodeCharacters.put("/", "/");
069 _bbCodeCharacters.put("`", "`");
070 _bbCodeCharacters.put("[", "[");
071 _bbCodeCharacters.put("]", "]");
072 _bbCodeCharacters.put("(", "(");
073 _bbCodeCharacters.put(")", ")");
074
075 for (int i = 0; i < _EMOTICONS.length; i++) {
076 String[] emoticon = _EMOTICONS[i];
077
078 _emoticonDescriptions[i] = emoticon[2];
079 _emoticonFiles[i] = emoticon[0];
080 _emoticonSymbols[i] = emoticon[1];
081
082 String image = emoticon[0];
083
084 emoticon[0] =
085 "<img alt=\"emoticon\" src=\"@theme_images_path@/emoticons/" +
086 image + "\" >";
087 }
088 }
089
090 @Override
091 public String[] getEmoticonDescriptions() {
092 return _emoticonDescriptions;
093 }
094
095 @Override
096 public String[] getEmoticonFiles() {
097 return _emoticonFiles;
098 }
099
100 @Override
101 public String[][] getEmoticons() {
102 return _EMOTICONS;
103 }
104
105 @Override
106 public String[] getEmoticonSymbols() {
107 return _emoticonSymbols;
108 }
109
110 @Override
111 public String getHTML(String bbcode) {
112 try {
113 bbcode = parse(bbcode);
114 }
115 catch (Exception e) {
116 _log.error("Unable to parse: " + bbcode, e);
117
118 bbcode = HtmlUtil.escape(bbcode);
119 }
120
121 return bbcode;
122 }
123
124 @Override
125 public String parse(String text) {
126 StringBundler sb = new StringBundler();
127
128 List<BBCodeItem> bbCodeItems = _bbCodeParser.parse(text);
129 Stack<String> tags = new Stack<String>();
130 IntegerWrapper marker = new IntegerWrapper();
131
132 for (; marker.getValue() < bbCodeItems.size(); marker.increment()) {
133 BBCodeItem bbCodeItem = bbCodeItems.get(marker.getValue());
134
135 int type = bbCodeItem.getType();
136
137 if (type == BBCodeParser.TYPE_DATA) {
138 handleData(sb, bbCodeItems, tags, marker, bbCodeItem);
139 }
140 else if (type == BBCodeParser.TYPE_TAG_END) {
141 handleTagEnd(sb, tags, bbCodeItem);
142 }
143 else if (type == BBCodeParser.TYPE_TAG_START) {
144 handleTagStart(sb, bbCodeItems, tags, marker, bbCodeItem);
145 }
146 }
147
148 return sb.toString();
149 }
150
151 protected String escapeQuote(String quote) {
152 StringBuilder sb = new StringBuilder();
153
154 int index = 0;
155
156 Matcher matcher = _bbCodePattern.matcher(quote);
157
158 Collection<String> values = _bbCodeCharacters.values();
159
160 while (matcher.find()) {
161 String match = matcher.group();
162
163 int matchStartIndex = matcher.start();
164
165 int nextSemicolonIndex = quote.indexOf(
166 StringPool.SEMICOLON, matchStartIndex);
167
168 sb.append(quote.substring(index, matchStartIndex));
169
170 boolean entityFound = false;
171
172 if (nextSemicolonIndex >= 0) {
173 String value = quote.substring(
174 matchStartIndex, nextSemicolonIndex + 1);
175
176 if (values.contains(value)) {
177 sb.append(value);
178
179 index = matchStartIndex + value.length();
180
181 entityFound = true;
182 }
183 }
184
185 if (!entityFound) {
186 String escapedValue = _bbCodeCharacters.get(match);
187
188 sb.append(escapedValue);
189
190 index = matchStartIndex + match.length();
191 }
192 }
193
194 if (index < quote.length()) {
195 sb.append(quote.substring(index, quote.length()));
196 }
197
198 return sb.toString();
199 }
200
201 protected String extractData(
202 List<BBCodeItem> bbCodeItems, IntegerWrapper marker, String tag,
203 int type, boolean consume) {
204
205 StringBundler sb = new StringBundler();
206
207 int index = marker.getValue() + 1;
208
209 BBCodeItem bbCodeItem = null;
210
211 do {
212 bbCodeItem = bbCodeItems.get(index++);
213
214 if ((bbCodeItem.getType() & type) > 0) {
215 sb.append(bbCodeItem.getValue());
216 }
217 }
218 while ((bbCodeItem.getType() != BBCodeParser.TYPE_TAG_END) &&
219 !tag.equals(bbCodeItem.getValue()));
220
221 if (consume) {
222 marker.setValue(index - 1);
223 }
224
225 return sb.toString();
226 }
227
228 protected void handleBold(StringBundler sb, Stack<String> tags) {
229 handleSimpleTag(sb, tags, "strong");
230 }
231
232 protected void handleCode(
233 StringBundler sb, List<BBCodeItem> bbCodeItems, IntegerWrapper marker) {
234
235 sb.append("<div class=\"lfr-code\">");
236 sb.append("<table>");
237 sb.append("<tbody>");
238 sb.append("<tr>");
239 sb.append("<td class=\"line-numbers\">");
240
241 String code = extractData(
242 bbCodeItems, marker, "code", BBCodeParser.TYPE_DATA, true);
243
244 code = HtmlUtil.escape(code);
245 code = code.replaceAll(StringPool.TAB, StringPool.FOUR_SPACES);
246
247 String[] lines = code.split("\r?\n");
248
249 for (int i = 0; i < lines.length; i++) {
250 String index = String.valueOf(i + 1);
251
252 sb.append("<span class=\"number\">");
253 sb.append(index);
254 sb.append("</span>");
255 }
256
257 sb.append("</td>");
258 sb.append("<td class=\"lines\">");
259
260 for (int i = 0; i < lines.length; i++) {
261 String line = lines[i];
262
263 line = StringUtil.replace(
264 line, StringPool.THREE_SPACES, " ");
265 line = StringUtil.replace(line, StringPool.DOUBLE_SPACE, " ");
266
267 if (Validator.isNull(line)) {
268 line = "<br />";
269 }
270
271 sb.append("<div class=\"line\">");
272 sb.append(line);
273 sb.append("</div>");
274 }
275
276 sb.append("</td>");
277 sb.append("</tr>");
278 sb.append("</tbody>");
279 sb.append("</table>");
280 sb.append("</div>");
281 }
282
283 protected void handleColor(
284 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
285
286 sb.append("<span style=\"color: ");
287
288 String color = bbCodeItem.getAttribute();
289
290 if (color == null) {
291 color = "inherit";
292 }
293 else {
294 Matcher matcher = _colorPattern.matcher(color);
295
296 if (!matcher.matches()) {
297 color = "inherit";
298 }
299 }
300
301 sb.append(color);
302
303 sb.append("\">");
304
305 tags.push("</span>");
306 }
307
308 protected void handleData(
309 StringBundler sb, List<BBCodeItem> bbCodeItems, Stack<String> tags,
310 IntegerWrapper marker, BBCodeItem bbCodeItem) {
311
312 String value = HtmlUtil.escape(bbCodeItem.getValue());
313
314 value = handleNewLine(bbCodeItems, tags, marker, value);
315
316 for (int i = 0; i < _EMOTICONS.length; i++) {
317 String[] emoticon = _EMOTICONS[i];
318
319 value = StringUtil.replace(value, emoticon[1], emoticon[0]);
320 }
321
322 sb.append(value);
323 }
324
325 protected void handleEmail(
326 StringBundler sb, List<BBCodeItem> bbCodeItems, Stack<String> tags,
327 IntegerWrapper marker, BBCodeItem bbCodeItem) {
328
329 sb.append("<a href=\"");
330
331 String href = bbCodeItem.getAttribute();
332
333 if (href == null) {
334 href = extractData(
335 bbCodeItems, marker, "email", BBCodeParser.TYPE_DATA, false);
336 }
337
338 if (!href.startsWith("mailto:")) {
339 href = "mailto:" + href;
340 }
341
342 sb.append(HtmlUtil.escapeHREF(href));
343
344 sb.append("\">");
345
346 tags.push("</a>");
347 }
348
349 protected void handleFontFamily(
350 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
351
352 sb.append("<span style=\"font-family: ");
353 sb.append(HtmlUtil.escapeAttribute(bbCodeItem.getAttribute()));
354 sb.append("\">");
355
356 tags.push("</span>");
357 }
358
359 protected void handleFontSize(
360 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
361
362 sb.append("<span style=\"font-size: ");
363
364 int size = GetterUtil.getInteger(bbCodeItem.getAttribute());
365
366 if ((size >= 1) && (size <= _fontSizes.length)) {
367 sb.append(_fontSizes[size - 1]);
368 }
369 else {
370 sb.append(_fontSizes[1]);
371 }
372
373 sb.append("px\">");
374
375 tags.push("</span>");
376 }
377
378 protected void handleImage(
379 StringBundler sb, List<BBCodeItem> bbCodeItems, IntegerWrapper marker) {
380
381 sb.append("<img src=\"");
382
383 String src = extractData(
384 bbCodeItems, marker, "img", BBCodeParser.TYPE_DATA, true);
385
386 Matcher matcher = _imagePattern.matcher(src);
387
388 if (matcher.matches()) {
389 sb.append(HtmlUtil.escapeAttribute(src));
390 }
391
392 sb.append("\" />");
393 }
394
395 protected void handleItalic(StringBundler sb, Stack<String> tags) {
396 handleSimpleTag(sb, tags, "em");
397 }
398
399 protected void handleList(
400 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
401
402 String listStyle = null;
403
404 String tag = null;
405
406 String listAttribute = bbCodeItem.getAttribute();
407
408 if (listAttribute != null) {
409 listStyle = _listStyles.get(listAttribute);
410
411 tag = "ol";
412 }
413 else {
414 tag = "ul style=\"list-style: disc outside;\"";
415 }
416
417 if (listStyle == null) {
418 sb.append("<");
419 sb.append(tag);
420 sb.append(">");
421 }
422 else {
423 sb.append("<");
424 sb.append(tag);
425 sb.append(" style=\"");
426 sb.append(listStyle);
427 sb.append("\">");
428 }
429
430 tags.push("</" + tag + ">");
431 }
432
433 protected void handleListItem(StringBundler sb, Stack<String> tags) {
434 handleSimpleTag(sb, tags, "li");
435 }
436
437 protected String handleNewLine(
438 List<BBCodeItem> bbCodeItems, Stack<String> tags, IntegerWrapper marker,
439 String data) {
440
441 BBCodeItem bbCodeItem = null;
442
443 if ((marker.getValue() + 1) < bbCodeItems.size()) {
444 if (data.matches("\\A\r?\n\\z")) {
445 bbCodeItem = bbCodeItems.get(marker.getValue() + 1);
446
447 if (bbCodeItem != null) {
448 String value = bbCodeItem.getValue();
449
450 if (_excludeNewLineTypes.containsKey(value)) {
451 int type = bbCodeItem.getType();
452
453 int excludeNewLineType = _excludeNewLineTypes.get(
454 value);
455
456 if ((type & excludeNewLineType) > 0) {
457 data = StringPool.BLANK;
458 }
459 }
460 }
461 }
462 else if (data.matches("(?s).*\r?\n\\z")) {
463 bbCodeItem = bbCodeItems.get(marker.getValue() + 1);
464
465 if ((bbCodeItem != null) &&
466 (bbCodeItem.getType() == BBCodeParser.TYPE_TAG_END)) {
467
468 String value = bbCodeItem.getValue();
469
470 if (value.equals("*")) {
471 data = data.substring(0, data.length() - 1);
472 }
473 }
474 }
475 }
476
477 if (data.length() > 0) {
478 data = data.replaceAll("\r?\n", "<br />");
479 }
480
481 return data;
482 }
483
484 protected void handleQuote(
485 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
486
487 String quote = bbCodeItem.getAttribute();
488
489 if ((quote != null) && (quote.length() > 0)) {
490 sb.append("<div class=\"quote-title\">");
491 sb.append(escapeQuote(quote));
492 sb.append(":</div>");
493 }
494
495 sb.append("<div class=\"quote\"><div class=\"quote-content\">");
496
497 tags.push("</div></div>");
498 }
499
500 protected void handleSimpleTag(
501 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
502
503 handleSimpleTag(sb, tags, bbCodeItem.getValue());
504 }
505
506 protected void handleSimpleTag(
507 StringBundler sb, Stack<String> tags, String tag) {
508
509 sb.append("<");
510 sb.append(tag);
511 sb.append(">");
512
513 tags.push("</" + tag + ">");
514 }
515
516 protected void handleStrikeThrough(StringBundler sb, Stack<String> tags) {
517 handleSimpleTag(sb, tags, "strike");
518 }
519
520 protected void handleTable(StringBundler sb, Stack<String> tags) {
521 handleSimpleTag(sb, tags, "table");
522 }
523
524 protected void handleTableCell(StringBundler sb, Stack<String> tags) {
525 handleSimpleTag(sb, tags, "td");
526 }
527
528 protected void handleTableHeader(StringBundler sb, Stack<String> tags) {
529 handleSimpleTag(sb, tags, "th");
530 }
531
532 protected void handleTableRow(StringBundler sb, Stack<String> tags) {
533 handleSimpleTag(sb, tags, "tr");
534 }
535
536 protected void handleTagEnd(
537 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
538
539 String tag = bbCodeItem.getValue();
540
541 if (isValidTag(tag)) {
542 sb.append(tags.pop());
543 }
544 }
545
546 protected void handleTagStart(
547 StringBundler sb, List<BBCodeItem> bbCodeItems, Stack<String> tags,
548 IntegerWrapper marker, BBCodeItem bbCodeItem) {
549
550 String tag = bbCodeItem.getValue();
551
552 if (!isValidTag(tag)) {
553 return;
554 }
555
556 if (tag.equals("b")) {
557 handleBold(sb, tags);
558 }
559 else if (tag.equals("center") || tag.equals("justify") ||
560 tag.equals("left") || tag.equals("right")) {
561
562 handleTextAlign(sb, tags, bbCodeItem);
563 }
564 else if (tag.equals("code")) {
565 handleCode(sb, bbCodeItems, marker);
566 }
567 else if (tag.equals("color") || tag.equals("colour")) {
568 handleColor(sb, tags, bbCodeItem);
569 }
570 else if (tag.equals("email")) {
571 handleEmail(sb, bbCodeItems, tags, marker, bbCodeItem);
572 }
573 else if (tag.equals("font")) {
574 handleFontFamily(sb, tags, bbCodeItem);
575 }
576 else if (tag.equals("i")) {
577 handleItalic(sb, tags);
578 }
579 else if (tag.equals("img")) {
580 handleImage(sb, bbCodeItems, marker);
581 }
582 else if (tag.equals("li") || tag.equals("*")) {
583 handleListItem(sb, tags);
584 }
585 else if (tag.equals("list")) {
586 handleList(sb, tags, bbCodeItem);
587 }
588 else if (tag.equals("q") || tag.equals("quote")) {
589 handleQuote(sb, tags, bbCodeItem);
590 }
591 else if (tag.equals("s")) {
592 handleStrikeThrough(sb, tags);
593 }
594 else if (tag.equals("size")) {
595 handleFontSize(sb, tags, bbCodeItem);
596 }
597 else if (tag.equals("table")) {
598 handleTable(sb, tags);
599 }
600 else if (tag.equals("td")) {
601 handleTableCell(sb, tags);
602 }
603 else if (tag.equals("th")) {
604 handleTableHeader(sb, tags);
605 }
606 else if (tag.equals("tr")) {
607 handleTableRow(sb, tags);
608 }
609 else if (tag.equals("url")) {
610 handleURL(sb, bbCodeItems, tags, marker, bbCodeItem);
611 }
612 else {
613 handleSimpleTag(sb, tags, bbCodeItem);
614 }
615 }
616
617 protected void handleTextAlign(
618 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
619
620 sb.append("<p style=\"text-align: ");
621 sb.append(bbCodeItem.getValue());
622 sb.append("\">");
623
624 tags.push("</p>");
625 }
626
627 protected void handleURL(
628 StringBundler sb, List<BBCodeItem> bbCodeItems, Stack<String> tags,
629 IntegerWrapper marker, BBCodeItem bbCodeItem) {
630
631 sb.append("<a href=\"");
632
633 String href = bbCodeItem.getAttribute();
634
635 if (href == null) {
636 href = extractData(
637 bbCodeItems, marker, "url", BBCodeParser.TYPE_DATA, false);
638 }
639
640 Matcher matcher = _urlPattern.matcher(href);
641
642 if (matcher.matches()) {
643 sb.append(HtmlUtil.escapeHREF(href));
644 }
645
646 sb.append("\">");
647
648 tags.push("</a>");
649 }
650
651 protected boolean isValidTag(String tag) {
652 if ((tag != null) && (tag.length() > 0)) {
653 Matcher matcher = _tagPattern.matcher(tag);
654
655 return matcher.matches();
656 }
657
658 return false;
659 }
660
661 private static final String[][] _EMOTICONS = {
662 {"happy.gif", ":)", "happy"},
663 {"smile.gif", ":D", "smile"},
664 {"cool.gif", "B)", "cool"},
665 {"sad.gif", ":(", "sad"},
666 {"tongue.gif", ":P", "tongue"},
667 {"laugh.gif", ":lol:", "laugh"},
668 {"kiss.gif", ":#", "kiss"},
669 {"blush.gif", ":*)", "blush"},
670 {"bashful.gif", ":bashful:", "bashful"},
671 {"smug.gif", ":smug:", "smug"},
672 {"blink.gif", ":blink:", "blink"},
673 {"huh.gif", ":huh:", "huh"},
674 {"mellow.gif", ":mellow:", "mellow"},
675 {"unsure.gif", ":unsure:", "unsure"},
676 {"mad.gif", ":mad:", "mad"},
677 {"oh_my.gif", ":O", "oh-my-goodness"},
678 {"roll_eyes.gif", ":rolleyes:", "roll-eyes"},
679 {"angry.gif", ":angry:", "angry"},
680 {"suspicious.gif", "8o", "suspicious"},
681 {"big_grin.gif", ":grin:", "grin"},
682 {"in_love.gif", ":love:", "in-love"},
683 {"bored.gif", ":bored:", "bored"},
684 {"closed_eyes.gif", "-_-", "closed-eyes"},
685 {"cold.gif", ":cold:", "cold"},
686 {"sleep.gif", ":sleep:", "sleep"},
687 {"glare.gif", ":glare:", "glare"},
688 {"darth_vader.gif", ":vader:", "darth-vader"},
689 {"dry.gif", ":dry:", "dry"},
690 {"exclamation.gif", ":what:", "what"},
691 {"girl.gif", ":girl:", "girl"},
692 {"karate_kid.gif", ":kid:", "karate-kid"},
693 {"ninja.gif", ":ph34r:", "ninja"},
694 {"pac_man.gif", ":V", "pac-man"},
695 {"wacko.gif", ":wacko:", "wacko"},
696 {"wink.gif", ":wink:", "wink"},
697 {"wub.gif", ":wub:", "wub"}
698 };
699
700 private static Log _log = LogFactoryUtil.getLog(
701 HtmlBBCodeTranslatorImpl.class);
702
703 private Map<String, String> _bbCodeCharacters;
704 private BBCodeParser _bbCodeParser = new BBCodeParser();
705 private Pattern _bbCodePattern = Pattern.compile("[]&<>'\"`\\[()]");
706 private Pattern _colorPattern = Pattern.compile(
707 "^(:?aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|purple" +
708 "|red|silver|teal|white|yellow|#(?:[0-9a-f]{3})?[0-9a-f]{3})$",
709 Pattern.CASE_INSENSITIVE);
710 private String[] _emoticonDescriptions = new String[_EMOTICONS.length];
711 private String[] _emoticonFiles = new String[_EMOTICONS.length];
712 private String[] _emoticonSymbols = new String[_EMOTICONS.length];
713 private Map<String, Integer> _excludeNewLineTypes;
714 private int[] _fontSizes = {10, 12, 16, 18, 24, 32, 48};
715 private Pattern _imagePattern = Pattern.compile(
716 "^(?:https?:
717 Pattern.CASE_INSENSITIVE);
718 private Map<String, String> _listStyles;
719 private Pattern _tagPattern = Pattern.compile(
720 "^/?(?:b|center|code|colou?r|email|i|img|justify|left|pre|q|quote|" +
721 "right|\\*|s|size|table|tr|th|td|li|list|font|u|url)$",
722 Pattern.CASE_INSENSITIVE);
723 private Pattern _urlPattern = Pattern.compile(
724 "^[-;/?:@&=+$,_.!~*'()%0-9a-z#]{1,512}$", Pattern.CASE_INSENSITIVE);
725
726 }