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