/**
* Copyright (c) 2000-present Liferay, Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*/
package com.liferay.message.boards.parser.bbcode.internal;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.model.ThemeConstants;
import com.liferay.portal.kernel.parsers.bbcode.BBCodeTranslator;
import com.liferay.portal.kernel.security.pacl.DoPrivileged;
import com.liferay.portal.kernel.util.CharPool;
import com.liferay.portal.kernel.util.GetterUtil;
import com.liferay.portal.kernel.util.HtmlUtil;
import com.liferay.portal.kernel.util.IntegerWrapper;
import com.liferay.portal.kernel.util.StringBundler;
import com.liferay.portal.kernel.util.StringPool;
import com.liferay.portal.kernel.util.StringUtil;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.portlet.messageboards.util.MBUtil;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.osgi.service.component.annotations.Component;
/**
* @author Iliyan Peychev
*/
@Component(service = BBCodeTranslator.class)
@DoPrivileged
public class HtmlBBCodeTranslatorImpl implements BBCodeTranslator {
public HtmlBBCodeTranslatorImpl() {
_bbCodeCharacters = new HashMap<>();
_bbCodeCharacters.put("&", "&");
_bbCodeCharacters.put("'", "'");
_bbCodeCharacters.put("(", "(");
_bbCodeCharacters.put(")", ")");
_bbCodeCharacters.put("/", "/");
_bbCodeCharacters.put("<", "<");
_bbCodeCharacters.put(">", ">");
_bbCodeCharacters.put("[", "[");
_bbCodeCharacters.put("\"", """);
_bbCodeCharacters.put("]", "]");
_bbCodeCharacters.put("`", "`");
for (int i = 0; i < _EMOTICONS.length; i++) {
String[] emoticon = _EMOTICONS[i];
_emoticonDescriptions[i] = emoticon[2];
_emoticonFiles[i] = emoticon[0];
_emoticonSymbols[i] = emoticon[1];
String image = emoticon[0];
StringBuilder sb = new StringBuilder(6);
sb.append("<img alt=\"emoticon\" src=\"");
sb.append(ThemeConstants.TOKEN_THEME_IMAGES_PATH);
sb.append(MBUtil.EMOTICONS);
sb.append("/");
sb.append(image);
sb.append("\" >");
emoticon[0] = sb.toString();
}
_excludeNewLineTypes = new HashMap<>();
_excludeNewLineTypes.put("*", BBCodeParser.TYPE_TAG_START_END);
_excludeNewLineTypes.put("li", BBCodeParser.TYPE_TAG_START_END);
_excludeNewLineTypes.put("table", BBCodeParser.TYPE_TAG_END);
_excludeNewLineTypes.put("td", BBCodeParser.TYPE_TAG_START_END);
_excludeNewLineTypes.put("th", BBCodeParser.TYPE_TAG_START_END);
_excludeNewLineTypes.put("tr", BBCodeParser.TYPE_TAG_START_END);
_imageAttributes = new HashSet<>(
Arrays.asList(
"alt", "class", "dir", "height", "id", "lang", "longdesc",
"style", "title", "width"));
_orderedListStyles = new HashMap<>();
_orderedListStyles.put("1", "list-style: decimal outside;");
_orderedListStyles.put("a", "list-style: lower-alpha outside;");
_orderedListStyles.put("A", "list-style: upper-alpha outside;");
_orderedListStyles.put("i", "list-style: lower-roman outside;");
_orderedListStyles.put("I", "list-style: upper-roman outside;");
_unorderedListStyles = new HashMap<>();
_unorderedListStyles.put("circle", "list-style: circle outside;");
_unorderedListStyles.put("disc", "list-style: disc outside;");
_unorderedListStyles.put("square", "list-style: square outside;");
}
@Override
public String[] getEmoticonDescriptions() {
return _emoticonDescriptions;
}
@Override
public String[] getEmoticonFiles() {
return _emoticonFiles;
}
@Override
public String[][] getEmoticons() {
return _EMOTICONS;
}
@Override
public String[] getEmoticonSymbols() {
return _emoticonSymbols;
}
@Override
public String getHTML(String bbcode) {
try {
bbcode = parse(bbcode);
}
catch (Exception e) {
_log.error("Unable to parse: " + bbcode, e);
bbcode = HtmlUtil.escape(bbcode);
}
return bbcode;
}
@Override
public String parse(String text) {
StringBundler sb = new StringBundler();
List<BBCodeItem> bbCodeItems = _bbCodeParser.parse(text);
Stack<String> tags = new Stack<>();
IntegerWrapper marker = new IntegerWrapper();
for (; marker.getValue() < bbCodeItems.size(); marker.increment()) {
BBCodeItem bbCodeItem = bbCodeItems.get(marker.getValue());
int type = bbCodeItem.getType();
if (type == BBCodeParser.TYPE_DATA) {
handleData(sb, bbCodeItems, tags, marker, bbCodeItem);
}
else if (type == BBCodeParser.TYPE_TAG_END) {
handleTagEnd(sb, tags);
}
else if (type == BBCodeParser.TYPE_TAG_START) {
handleTagStart(sb, bbCodeItems, tags, marker, bbCodeItem);
}
}
return sb.toString();
}
protected String escapeQuote(String quote) {
StringBuilder sb = new StringBuilder();
int index = 0;
Matcher matcher = _bbCodePattern.matcher(quote);
Collection<String> values = _bbCodeCharacters.values();
while (matcher.find()) {
String match = matcher.group();
int matchStartIndex = matcher.start();
int nextSemicolonIndex = quote.indexOf(
StringPool.SEMICOLON, matchStartIndex);
sb.append(quote.substring(index, matchStartIndex));
boolean entityFound = false;
if (nextSemicolonIndex >= 0) {
String value = quote.substring(
matchStartIndex, nextSemicolonIndex + 1);
if (values.contains(value)) {
sb.append(value);
index = matchStartIndex + value.length();
entityFound = true;
}
}
if (!entityFound) {
String escapedValue = _bbCodeCharacters.get(match);
sb.append(escapedValue);
index = matchStartIndex + match.length();
}
}
if (index < quote.length()) {
sb.append(quote.substring(index, quote.length()));
}
return sb.toString();
}
protected String extractData(
List<BBCodeItem> bbCodeItems, IntegerWrapper marker, String tag,
int type, boolean consume) {
StringBundler sb = new StringBundler();
int index = marker.getValue() + 1;
BBCodeItem bbCodeItem = null;
do {
bbCodeItem = bbCodeItems.get(index++);
if ((bbCodeItem.getType() & type) > 0) {
sb.append(bbCodeItem.getValue());
}
}
while ((bbCodeItem.getType() != BBCodeParser.TYPE_TAG_END) &&
!tag.equals(bbCodeItem.getValue()));
if (consume) {
marker.setValue(index - 1);
}
return sb.toString();
}
protected void handleBold(StringBundler sb, Stack<String> tags) {
handleSimpleTag(sb, tags, "strong");
}
protected void handleCode(
StringBundler sb, List<BBCodeItem> bbCodeItems, IntegerWrapper marker) {
sb.append("<div class=\"lfr-code\">");
sb.append("<table>");
sb.append("<tbody>");
String code = extractData(
bbCodeItems, marker, "code", BBCodeParser.TYPE_DATA, true);
code = HtmlUtil.escape(code);
code = StringUtil.replace(code, CharPool.TAB, StringPool.FOUR_SPACES);
String[] lines = code.split("\r?\n");
for (int i = 0; i < lines.length; i++) {
sb.append("<tr>");
sb.append("<td class=\"line-numbers\" data-line-number=\"");
String index = String.valueOf(i + 1);
sb.append(index);
sb.append("\"></td>");
sb.append("<td class=\"lines\">");
String line = lines[i];
line = StringUtil.replace(
line, StringPool.THREE_SPACES, " ");
line = StringUtil.replace(line, StringPool.DOUBLE_SPACE, " ");
if (Validator.isNull(line)) {
line = "<br />";
}
sb.append("<div class=\"line\">");
sb.append(line);
sb.append("</div>");
sb.append("</td>");
sb.append("</tr>");
}
sb.append("</tbody>");
sb.append("</table>");
sb.append("</div>");
}
protected void handleColor(
StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
sb.append("<span style=\"color: ");
String color = bbCodeItem.getAttribute();
if (color == null) {
color = "inherit";
}
else {
Matcher matcher = _colorPattern.matcher(color);
if (!matcher.matches()) {
color = "inherit";
}
}
sb.append(color);
sb.append("\">");
tags.push("</span>");
}
protected void handleData(
StringBundler sb, List<BBCodeItem> bbCodeItems, Stack<String> tags,
IntegerWrapper marker, BBCodeItem bbCodeItem) {
String value = HtmlUtil.escape(bbCodeItem.getValue());
value = handleNewLine(bbCodeItems, tags, marker, value);
for (String[] emoticon : _EMOTICONS) {
value = StringUtil.replace(value, emoticon[1], emoticon[0]);
}
sb.append(value);
}
protected void handleEmail(
StringBundler sb, List<BBCodeItem> bbCodeItems, Stack<String> tags,
IntegerWrapper marker, BBCodeItem bbCodeItem) {
sb.append("<a href=\"");
String href = bbCodeItem.getAttribute();
if (href == null) {
href = extractData(
bbCodeItems, marker, "email", BBCodeParser.TYPE_DATA, false);
}
if (!href.startsWith("mailto:")) {
href = "mailto:" + href;
}
sb.append(HtmlUtil.escapeHREF(href));
sb.append("\">");
tags.push("</a>");
}
protected void handleFontFamily(
StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
sb.append("<span style=\"font-family: ");
sb.append(HtmlUtil.escapeAttribute(bbCodeItem.getAttribute()));
sb.append("\">");
tags.push("</span>");
}
protected void handleFontSize(
StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
sb.append("<span style=\"font-size: ");
int size = GetterUtil.getInteger(bbCodeItem.getAttribute());
if ((size >= 1) && (size <= _fontSizes.length)) {
sb.append(_fontSizes[size - 1]);
}
else {
sb.append(_fontSizes[1]);
}
sb.append("px;\">");
tags.push("</span>");
}
protected void handleImage(
StringBundler sb, Stack<String> tags, List<BBCodeItem> bbCodeItems,
IntegerWrapper marker) {
sb.append("<img src=\"");
int pos = marker.getValue();
String src = extractData(
bbCodeItems, marker, "img", BBCodeParser.TYPE_DATA, true);
Matcher matcher = _imagePattern.matcher(src);
if (matcher.matches()) {
sb.append(HtmlUtil.escapeAttribute(src));
}
sb.append("\"");
BBCodeItem bbCodeItem = bbCodeItems.get(pos);
String attributes = bbCodeItem.getAttribute();
if (Validator.isNotNull(attributes)) {
sb.append(StringPool.SPACE);
handleImageAttributes(sb, attributes);
}
sb.append(" />");
tags.push(StringPool.BLANK);
}
protected void handleImageAttributes(StringBundler sb, String attributes) {
Matcher matcher = _attributesPattern.matcher(attributes);
while (matcher.find()) {
String attributeName = matcher.group(1);
if (Validator.isNotNull(attributeName) &&
_imageAttributes.contains(
StringUtil.toLowerCase(attributeName))) {
String attributeValue = matcher.group(2);
sb.append(StringPool.SPACE);
sb.append(attributeName);
sb.append(StringPool.EQUAL);
sb.append(StringPool.QUOTE);
sb.append(HtmlUtil.escapeAttribute(attributeValue));
sb.append(StringPool.QUOTE);
}
}
}
protected void handleItalic(StringBundler sb, Stack<String> tags) {
handleSimpleTag(sb, tags, "em");
}
protected void handleList(
StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
String tag = "ul";
StringBundler attributesSB = new StringBundler();
if (Validator.isNotNull(bbCodeItem.getAttribute())) {
Matcher matcher = _attributesPattern.matcher(
bbCodeItem.getAttribute());
while (matcher.find()) {
String listStyle = null;
String attributeName = matcher.group(1);
String attributeValue = matcher.group(2);
if (Objects.equals(attributeName, "type")) {
if (_orderedListStyles.get(attributeValue) != null) {
listStyle = _orderedListStyles.get(attributeValue);
tag = "ol";
}
else {
listStyle = _unorderedListStyles.get(attributeValue);
}
if (Validator.isNotNull(listStyle)) {
attributesSB.append(" style=\"");
attributesSB.append(listStyle);
attributesSB.append("\"");
}
}
else if (Objects.equals(attributeName, "start") &&
Validator.isNumber(attributeValue)) {
attributesSB.append(" start=\"");
attributesSB.append(attributeValue);
attributesSB.append("\"");
}
}
}
sb.append("<");
sb.append(tag);
sb.append(attributesSB);
sb.append(">");
tags.push("</" + tag + ">");
}
protected void handleListItem(StringBundler sb, Stack<String> tags) {
handleSimpleTag(sb, tags, "li");
}
protected String handleNewLine(
List<BBCodeItem> bbCodeItems, Stack<String> tags, IntegerWrapper marker,
String data) {
BBCodeItem bbCodeItem = null;
if ((marker.getValue() + 1) < bbCodeItems.size()) {
if (data.matches("\\A\r?\n\\z")) {
bbCodeItem = bbCodeItems.get(marker.getValue() + 1);
if (bbCodeItem != null) {
String value = bbCodeItem.getValue();
if (_excludeNewLineTypes.containsKey(value)) {
int type = bbCodeItem.getType();
int excludeNewLineType = _excludeNewLineTypes.get(
value);
if ((type & excludeNewLineType) > 0) {
data = StringPool.BLANK;
}
}
}
}
else if (data.matches("(?s).*\r?\n\\z")) {
bbCodeItem = bbCodeItems.get(marker.getValue() + 1);
if ((bbCodeItem != null) &&
(bbCodeItem.getType() == BBCodeParser.TYPE_TAG_END)) {
String value = bbCodeItem.getValue();
if (value.equals("*")) {
data = data.substring(0, data.length() - 1);
}
}
}
}
if (data.length() > 0) {
data = StringUtil.replace(
data, StringPool.RETURN_NEW_LINE, "<br />");
data = StringUtil.replace(data, CharPool.NEW_LINE, "<br />");
}
return data;
}
protected void handleQuote(
StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
String quote = bbCodeItem.getAttribute();
if ((quote != null) && (quote.length() > 0)) {
sb.append("<div class=\"quote-title\">");
sb.append(escapeQuote(quote));
sb.append(":</div>");
}
sb.append("<div class=\"quote\"><div class=\"quote-content\">");
tags.push("</div></div>");
}
protected void handleSimpleTag(
StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
handleSimpleTag(sb, tags, bbCodeItem.getValue());
}
protected void handleSimpleTag(
StringBundler sb, Stack<String> tags, String tag) {
sb.append("<");
sb.append(tag);
sb.append(">");
tags.push("</" + tag + ">");
}
protected void handleStrikeThrough(StringBundler sb, Stack<String> tags) {
handleSimpleTag(sb, tags, "strike");
}
protected void handleTable(StringBundler sb, Stack<String> tags) {
handleSimpleTag(sb, tags, "table");
}
protected void handleTableCell(StringBundler sb, Stack<String> tags) {
handleSimpleTag(sb, tags, "td");
}
protected void handleTableHeader(StringBundler sb, Stack<String> tags) {
handleSimpleTag(sb, tags, "th");
}
protected void handleTableRow(StringBundler sb, Stack<String> tags) {
handleSimpleTag(sb, tags, "tr");
}
protected void handleTagEnd(StringBundler sb, Stack<String> tags) {
sb.append(tags.pop());
}
protected void handleTagStart(
StringBundler sb, List<BBCodeItem> bbCodeItems, Stack<String> tags,
IntegerWrapper marker, BBCodeItem bbCodeItem) {
String tag = bbCodeItem.getValue();
if (tag.equals("b")) {
handleBold(sb, tags);
}
else if (tag.equals("center") || tag.equals("justify") ||
tag.equals("left") || tag.equals("right")) {
handleTextAlign(sb, tags, bbCodeItem);
}
else if (tag.equals("code")) {
handleCode(sb, bbCodeItems, marker);
}
else if (tag.equals("color") || tag.equals("colour")) {
handleColor(sb, tags, bbCodeItem);
}
else if (tag.equals("email")) {
handleEmail(sb, bbCodeItems, tags, marker, bbCodeItem);
}
else if (tag.equals("font")) {
handleFontFamily(sb, tags, bbCodeItem);
}
else if (tag.equals("i")) {
handleItalic(sb, tags);
}
else if (tag.equals("img")) {
handleImage(sb, tags, bbCodeItems, marker);
}
else if (tag.equals("li") || tag.equals("*")) {
handleListItem(sb, tags);
}
else if (tag.equals("list")) {
handleList(sb, tags, bbCodeItem);
}
else if (tag.equals("q") || tag.equals("quote")) {
handleQuote(sb, tags, bbCodeItem);
}
else if (tag.equals("s")) {
handleStrikeThrough(sb, tags);
}
else if (tag.equals("size")) {
handleFontSize(sb, tags, bbCodeItem);
}
else if (tag.equals("table")) {
handleTable(sb, tags);
}
else if (tag.equals("td")) {
handleTableCell(sb, tags);
}
else if (tag.equals("th")) {
handleTableHeader(sb, tags);
}
else if (tag.equals("tr")) {
handleTableRow(sb, tags);
}
else if (tag.equals("url")) {
handleURL(sb, bbCodeItems, tags, marker, bbCodeItem);
}
else {
handleSimpleTag(sb, tags, bbCodeItem);
}
}
protected void handleTextAlign(
StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
sb.append("<p style=\"text-align: ");
sb.append(bbCodeItem.getValue());
sb.append("\">");
tags.push("</p>");
}
protected void handleURL(
StringBundler sb, List<BBCodeItem> bbCodeItems, Stack<String> tags,
IntegerWrapper marker, BBCodeItem bbCodeItem) {
sb.append("<a href=\"");
String href = bbCodeItem.getAttribute();
if (href == null) {
href = extractData(
bbCodeItems, marker, "url", BBCodeParser.TYPE_DATA, false);
}
Matcher matcher = _urlPattern.matcher(href);
if (matcher.matches()) {
sb.append(HtmlUtil.escapeHREF(href));
}
sb.append("\">");
tags.push("</a>");
}
private static final String[][] _EMOTICONS = {
{"happy.gif", ":)", "happy"}, {"smile.gif", ":D", "smile"},
{"cool.gif", "B)", "cool"}, {"sad.gif", ":(", "sad"},
{"tongue.gif", ":P", "tongue"}, {"laugh.gif", ":lol:", "laugh"},
{"kiss.gif", ":#", "kiss"}, {"blush.gif", ":*)", "blush"},
{"bashful.gif", ":bashful:", "bashful"}, {"smug.gif", ":smug:", "smug"},
{"blink.gif", ":blink:", "blink"}, {"huh.gif", ":huh:", "huh"},
{"mellow.gif", ":mellow:", "mellow"},
{"unsure.gif", ":unsure:", "unsure"}, {"mad.gif", ":mad:", "mad"},
{"oh_my.gif", ":O", "oh-my-goodness"},
{"roll_eyes.gif", ":rolleyes:", "roll-eyes"},
{"angry.gif", ":angry:", "angry"},
{"suspicious.gif", "8o", "suspicious"},
{"big_grin.gif", ":grin:", "grin"},
{"in_love.gif", ":love:", "in-love"}, {"bored.gif", ":bored:", "bored"},
{"closed_eyes.gif", "-_-", "closed-eyes"},
{"cold.gif", ":cold:", "cold"}, {"sleep.gif", ":sleep:", "sleep"},
{"glare.gif", ":glare:", "glare"},
{"darth_vader.gif", ":vader:", "darth-vader"},
{"dry.gif", ":dry:", "dry"}, {"exclamation.gif", ":what:", "what"},
{"girl.gif", ":girl:", "girl"},
{"karate_kid.gif", ":kid:", "karate-kid"},
{"ninja.gif", ":ph34r:", "ninja"}, {"pac_man.gif", ":V", "pac-man"},
{"wacko.gif", ":wacko:", "wacko"}, {"wink.gif", ":wink:", "wink"},
{"wub.gif", ":wub:", "wub"}
};
private static final Log _log = LogFactoryUtil.getLog(
HtmlBBCodeTranslatorImpl.class);
private final Pattern _attributesPattern = Pattern.compile(
"\\s*([^=]+)\\s*=\\s*\"([^\"]+)\"\\s*");
private final Map<String, String> _bbCodeCharacters;
private final BBCodeParser _bbCodeParser = new BBCodeParser();
private final Pattern _bbCodePattern = Pattern.compile("[]&<>'\"`\\[()]");
private final Pattern _colorPattern = Pattern.compile(
"^(:?aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|purple" +
"|red|silver|teal|white|yellow|#(?:[0-9a-f]{3})?[0-9a-f]{3})$",
Pattern.CASE_INSENSITIVE);
private final String[] _emoticonDescriptions =
new String[_EMOTICONS.length];
private final String[] _emoticonFiles = new String[_EMOTICONS.length];
private final String[] _emoticonSymbols = new String[_EMOTICONS.length];
private final Map<String, Integer> _excludeNewLineTypes;
private final int[] _fontSizes = {10, 12, 14, 16, 18, 24, 32, 48};
private final Set<String> _imageAttributes;
private final Pattern _imagePattern = Pattern.compile(
"^(?:https?://|/)[-;/?:@&=+$,_.!~*'()%0-9a-z]{1,2048}$",
Pattern.CASE_INSENSITIVE);
private final Map<String, String> _orderedListStyles;
private final Map<String, String> _unorderedListStyles;
private final Pattern _urlPattern = Pattern.compile(
"^[-;/?:@&=+$,_.!~*'()%0-9a-z#]{1,2048}$", Pattern.CASE_INSENSITIVE);
}