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 }
219 while ((bbCodeItem.getType() != BBCodeParser.TYPE_TAG_END) &&
220 !tag.equals(bbCodeItem.getValue()));
221
222 if (consume) {
223 marker.setValue(index - 1);
224 }
225
226 return sb.toString();
227 }
228
229 protected void handleBold(StringBundler sb, Stack<String> tags) {
230 handleSimpleTag(sb, tags, "strong");
231 }
232
233 protected void handleCode(
234 StringBundler sb, List<BBCodeItem> bbCodeItems, IntegerWrapper marker) {
235
236 sb.append("<div class=\"lfr-code\">");
237 sb.append("<table>");
238 sb.append("<tbody>");
239 sb.append("<tr>");
240 sb.append("<td class=\"line-numbers\">");
241
242 String code = extractData(
243 bbCodeItems, marker, "code", BBCodeParser.TYPE_DATA, true);
244
245 code = HtmlUtil.escape(code);
246 code = code.replaceAll(StringPool.TAB, StringPool.FOUR_SPACES);
247
248 String[] lines = code.split("\r?\n");
249
250 for (int i = 0; i < lines.length; i++) {
251 String index = String.valueOf(i + 1);
252
253 sb.append("<span class=\"number\">");
254 sb.append(index);
255 sb.append("</span>");
256 }
257
258 sb.append("</td>");
259 sb.append("<td class=\"lines\">");
260
261 for (int i = 0; i < lines.length; i++) {
262 String line = lines[i];
263
264 line = StringUtil.replace(
265 line, StringPool.THREE_SPACES, " ");
266 line = StringUtil.replace(line, StringPool.DOUBLE_SPACE, " ");
267
268 if (Validator.isNull(line)) {
269 line = "<br />";
270 }
271
272 sb.append("<div class=\"line\">");
273 sb.append(line);
274 sb.append("</div>");
275 }
276
277 sb.append("</td>");
278 sb.append("</tr>");
279 sb.append("</tbody>");
280 sb.append("</table>");
281 sb.append("</div>");
282 }
283
284 protected void handleColor(
285 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
286
287 sb.append("<span style=\"color: ");
288
289 String color = bbCodeItem.getAttribute();
290
291 if (color == null) {
292 color = "inherit";
293 }
294 else {
295 Matcher matcher = _colorPattern.matcher(color);
296
297 if (!matcher.matches()) {
298 color = "inherit";
299 }
300 }
301
302 sb.append(color);
303
304 sb.append("\">");
305
306 tags.push("</span>");
307 }
308
309 protected void handleData(
310 StringBundler sb, List<BBCodeItem> bbCodeItems, Stack<String> tags,
311 IntegerWrapper marker, BBCodeItem bbCodeItem) {
312
313 String value = HtmlUtil.escape(bbCodeItem.getValue());
314
315 value = handleNewLine(bbCodeItems, tags, marker, value);
316
317 for (int i = 0; i < _EMOTICONS.length; i++) {
318 String[] emoticon = _EMOTICONS[i];
319
320 value = StringUtil.replace(value, emoticon[1], emoticon[0]);
321 }
322
323 sb.append(value);
324 }
325
326 protected void handleEmail(
327 StringBundler sb, List<BBCodeItem> bbCodeItems, Stack<String> tags,
328 IntegerWrapper marker, BBCodeItem bbCodeItem) {
329
330 sb.append("<a href=\"");
331
332 String href = bbCodeItem.getAttribute();
333
334 if (href == null) {
335 href = extractData(
336 bbCodeItems, marker, "email", BBCodeParser.TYPE_DATA, false);
337 }
338
339 if (!href.startsWith("mailto:")) {
340 href = "mailto:" + href;
341 }
342
343 sb.append(HtmlUtil.escapeHREF(href));
344
345 sb.append("\">");
346
347 tags.push("</a>");
348 }
349
350 protected void handleFontFamily(
351 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
352
353 sb.append("<span style=\"font-family: ");
354 sb.append(HtmlUtil.escapeAttribute(bbCodeItem.getAttribute()));
355 sb.append("\">");
356
357 tags.push("</span>");
358 }
359
360 protected void handleFontSize(
361 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
362
363 sb.append("<span style=\"font-size: ");
364
365 int size = GetterUtil.getInteger(bbCodeItem.getAttribute());
366
367 if ((size >= 1) && (size <= _fontSizes.length)) {
368 sb.append(_fontSizes[size - 1]);
369 }
370 else {
371 sb.append(_fontSizes[1]);
372 }
373
374 sb.append("px\">");
375
376 tags.push("</span>");
377 }
378
379 protected void handleImage(
380 StringBundler sb, List<BBCodeItem> bbCodeItems, IntegerWrapper marker) {
381
382 sb.append("<img src=\"");
383
384 String src = extractData(
385 bbCodeItems, marker, "img", BBCodeParser.TYPE_DATA, true);
386
387 Matcher matcher = _imagePattern.matcher(src);
388
389 if (matcher.matches()) {
390 sb.append(HtmlUtil.escapeAttribute(src));
391 }
392
393 sb.append("\" />");
394 }
395
396 protected void handleItalic(StringBundler sb, Stack<String> tags) {
397 handleSimpleTag(sb, tags, "em");
398 }
399
400 protected void handleList(
401 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
402
403 String listStyle = null;
404
405 String tag = null;
406
407 String listAttribute = bbCodeItem.getAttribute();
408
409 if (listAttribute != null) {
410 listStyle = _listStyles.get(listAttribute);
411
412 tag = "ol";
413 }
414 else {
415 tag = "ul style=\"list-style: disc outside;\"";
416 }
417
418 if (listStyle == null) {
419 sb.append("<");
420 sb.append(tag);
421 sb.append(">");
422 }
423 else {
424 sb.append("<");
425 sb.append(tag);
426 sb.append(" style=\"");
427 sb.append(listStyle);
428 sb.append("\">");
429 }
430
431 tags.push("</" + tag + ">");
432 }
433
434 protected void handleListItem(StringBundler sb, Stack<String> tags) {
435 handleSimpleTag(sb, tags, "li");
436 }
437
438 protected String handleNewLine(
439 List<BBCodeItem> bbCodeItems, Stack<String> tags, IntegerWrapper marker,
440 String data) {
441
442 BBCodeItem bbCodeItem = null;
443
444 if ((marker.getValue() + 1) < bbCodeItems.size()) {
445 if (data.matches("\\A\r?\n\\z")) {
446 bbCodeItem = bbCodeItems.get(marker.getValue() + 1);
447
448 if (bbCodeItem != null) {
449 String value = bbCodeItem.getValue();
450
451 if (_excludeNewLineTypes.containsKey(value)) {
452 int type = bbCodeItem.getType();
453
454 int excludeNewLineType = _excludeNewLineTypes.get(
455 value);
456
457 if ((type & excludeNewLineType) > 0) {
458 data = StringPool.BLANK;
459 }
460 }
461 }
462 }
463 else if (data.matches("(?s).*\r?\n\\z")) {
464 bbCodeItem = bbCodeItems.get(marker.getValue() + 1);
465
466 if ((bbCodeItem != null) &&
467 (bbCodeItem.getType() == BBCodeParser.TYPE_TAG_END)) {
468
469 String value = bbCodeItem.getValue();
470
471 if (value.equals("*")) {
472 data = data.substring(0, data.length() - 1);
473 }
474 }
475 }
476 }
477
478 if (data.length() > 0) {
479 data = data.replaceAll("\r?\n", "<br />");
480 }
481
482 return data;
483 }
484
485 protected void handleQuote(
486 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
487
488 String quote = bbCodeItem.getAttribute();
489
490 if ((quote != null) && (quote.length() > 0)) {
491 sb.append("<div class=\"quote-title\">");
492 sb.append(escapeQuote(quote));
493 sb.append(":</div>");
494 }
495
496 sb.append("<div class=\"quote\"><div class=\"quote-content\">");
497
498 tags.push("</div></div>");
499 }
500
501 protected void handleSimpleTag(
502 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
503
504 handleSimpleTag(sb, tags, bbCodeItem.getValue());
505 }
506
507 protected void handleSimpleTag(
508 StringBundler sb, Stack<String> tags, String tag) {
509
510 sb.append("<");
511 sb.append(tag);
512 sb.append(">");
513
514 tags.push("</" + tag + ">");
515 }
516
517 protected void handleStrikeThrough(StringBundler sb, Stack<String> tags) {
518 handleSimpleTag(sb, tags, "strike");
519 }
520
521 protected void handleTable(StringBundler sb, Stack<String> tags) {
522 handleSimpleTag(sb, tags, "table");
523 }
524
525 protected void handleTableCell(StringBundler sb, Stack<String> tags) {
526 handleSimpleTag(sb, tags, "td");
527 }
528
529 protected void handleTableHeader(StringBundler sb, Stack<String> tags) {
530 handleSimpleTag(sb, tags, "th");
531 }
532
533 protected void handleTableRow(StringBundler sb, Stack<String> tags) {
534 handleSimpleTag(sb, tags, "tr");
535 }
536
537 protected void handleTagEnd(
538 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
539
540 String tag = bbCodeItem.getValue();
541
542 if (isValidTag(tag)) {
543 sb.append(tags.pop());
544 }
545 }
546
547 protected void handleTagStart(
548 StringBundler sb, List<BBCodeItem> bbCodeItems, Stack<String> tags,
549 IntegerWrapper marker, BBCodeItem bbCodeItem) {
550
551 String tag = bbCodeItem.getValue();
552
553 if (!isValidTag(tag)) {
554 return;
555 }
556
557 if (tag.equals("b")) {
558 handleBold(sb, tags);
559 }
560 else if (tag.equals("center") || tag.equals("justify") ||
561 tag.equals("left") || tag.equals("right")) {
562
563 handleTextAlign(sb, tags, bbCodeItem);
564 }
565 else if (tag.equals("code")) {
566 handleCode(sb, bbCodeItems, marker);
567 }
568 else if (tag.equals("color") || tag.equals("colour")) {
569 handleColor(sb, tags, bbCodeItem);
570 }
571 else if (tag.equals("email")) {
572 handleEmail(sb, bbCodeItems, tags, marker, bbCodeItem);
573 }
574 else if (tag.equals("font")) {
575 handleFontFamily(sb, tags, bbCodeItem);
576 }
577 else if (tag.equals("i")) {
578 handleItalic(sb, tags);
579 }
580 else if (tag.equals("img")) {
581 handleImage(sb, bbCodeItems, marker);
582 }
583 else if (tag.equals("li") || tag.equals("*")) {
584 handleListItem(sb, tags);
585 }
586 else if (tag.equals("list")) {
587 handleList(sb, tags, bbCodeItem);
588 }
589 else if (tag.equals("q") || tag.equals("quote")) {
590 handleQuote(sb, tags, bbCodeItem);
591 }
592 else if (tag.equals("s")) {
593 handleStrikeThrough(sb, tags);
594 }
595 else if (tag.equals("size")) {
596 handleFontSize(sb, tags, bbCodeItem);
597 }
598 else if (tag.equals("table")) {
599 handleTable(sb, tags);
600 }
601 else if (tag.equals("td")) {
602 handleTableCell(sb, tags);
603 }
604 else if (tag.equals("th")) {
605 handleTableHeader(sb, tags);
606 }
607 else if (tag.equals("tr")) {
608 handleTableRow(sb, tags);
609 }
610 else if (tag.equals("url")) {
611 handleURL(sb, bbCodeItems, tags, marker, bbCodeItem);
612 }
613 else {
614 handleSimpleTag(sb, tags, bbCodeItem);
615 }
616 }
617
618 protected void handleTextAlign(
619 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
620
621 sb.append("<p style=\"text-align: ");
622 sb.append(bbCodeItem.getValue());
623 sb.append("\">");
624
625 tags.push("</p>");
626 }
627
628 protected void handleURL(
629 StringBundler sb, List<BBCodeItem> bbCodeItems, Stack<String> tags,
630 IntegerWrapper marker, BBCodeItem bbCodeItem) {
631
632 sb.append("<a href=\"");
633
634 String href = bbCodeItem.getAttribute();
635
636 if (href == null) {
637 href = extractData(
638 bbCodeItems, marker, "url", BBCodeParser.TYPE_DATA, false);
639 }
640
641 Matcher matcher = _urlPattern.matcher(href);
642
643 if (matcher.matches()) {
644 sb.append(HtmlUtil.escapeHREF(href));
645 }
646
647 sb.append("\">");
648
649 tags.push("</a>");
650 }
651
652 protected boolean isValidTag(String tag) {
653 if ((tag != null) && (tag.length() > 0)) {
654 Matcher matcher = _tagPattern.matcher(tag);
655
656 return matcher.matches();
657 }
658
659 return false;
660 }
661
662 private static final String[][] _EMOTICONS = {
663 {"happy.gif", ":)", "happy"},
664 {"smile.gif", ":D", "smile"},
665 {"cool.gif", "B)", "cool"},
666 {"sad.gif", ":(", "sad"},
667 {"tongue.gif", ":P", "tongue"},
668 {"laugh.gif", ":lol:", "laugh"},
669 {"kiss.gif", ":#", "kiss"},
670 {"blush.gif", ":*)", "blush"},
671 {"bashful.gif", ":bashful:", "bashful"},
672 {"smug.gif", ":smug:", "smug"},
673 {"blink.gif", ":blink:", "blink"},
674 {"huh.gif", ":huh:", "huh"},
675 {"mellow.gif", ":mellow:", "mellow"},
676 {"unsure.gif", ":unsure:", "unsure"},
677 {"mad.gif", ":mad:", "mad"},
678 {"oh_my.gif", ":O", "oh-my-goodness"},
679 {"roll_eyes.gif", ":rolleyes:", "roll-eyes"},
680 {"angry.gif", ":angry:", "angry"},
681 {"suspicious.gif", "8o", "suspicious"},
682 {"big_grin.gif", ":grin:", "grin"},
683 {"in_love.gif", ":love:", "in-love"},
684 {"bored.gif", ":bored:", "bored"},
685 {"closed_eyes.gif", "-_-", "closed-eyes"},
686 {"cold.gif", ":cold:", "cold"},
687 {"sleep.gif", ":sleep:", "sleep"},
688 {"glare.gif", ":glare:", "glare"},
689 {"darth_vader.gif", ":vader:", "darth-vader"},
690 {"dry.gif", ":dry:", "dry"},
691 {"exclamation.gif", ":what:", "what"},
692 {"girl.gif", ":girl:", "girl"},
693 {"karate_kid.gif", ":kid:", "karate-kid"},
694 {"ninja.gif", ":ph34r:", "ninja"},
695 {"pac_man.gif", ":V", "pac-man"},
696 {"wacko.gif", ":wacko:", "wacko"},
697 {"wink.gif", ":wink:", "wink"},
698 {"wub.gif", ":wub:", "wub"}
699 };
700
701 private static Log _log = LogFactoryUtil.getLog(
702 HtmlBBCodeTranslatorImpl.class);
703
704 private Map<String, String> _bbCodeCharacters;
705 private BBCodeParser _bbCodeParser = new BBCodeParser();
706 private Pattern _bbCodePattern = Pattern.compile("[]&<>'\"`\\[()]");
707 private Pattern _colorPattern = Pattern.compile(
708 "^(:?aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|purple" +
709 "|red|silver|teal|white|yellow|#(?:[0-9a-f]{3})?[0-9a-f]{3})$",
710 Pattern.CASE_INSENSITIVE);
711 private String[] _emoticonDescriptions = new String[_EMOTICONS.length];
712 private String[] _emoticonFiles = new String[_EMOTICONS.length];
713 private String[] _emoticonSymbols = new String[_EMOTICONS.length];
714 private Map<String, Integer> _excludeNewLineTypes;
715 private int[] _fontSizes = {10, 12, 16, 18, 24, 32, 48};
716 private Pattern _imagePattern = Pattern.compile(
717 "^(?:https?:
718 Pattern.CASE_INSENSITIVE);
719 private Map<String, String> _listStyles;
720 private Pattern _tagPattern = Pattern.compile(
721 "^/?(?:b|center|code|colou?r|email|i|img|justify|left|pre|q|quote|" +
722 "right|\\*|s|size|table|tr|th|td|li|list|font|u|url)$",
723 Pattern.CASE_INSENSITIVE);
724 private Pattern _urlPattern = Pattern.compile(
725 "^[-;/?:@&=+$,_.!~*'()%0-9a-z#]{1,512}$", Pattern.CASE_INSENSITIVE);
726
727 }