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