/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This 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 software 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. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.xwiki.wysiwyg.server.filter; import java.io.IOException; import java.lang.reflect.Type; import java.util.HashMap; import java.util.Map; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xwiki.gwt.wysiwyg.client.converter.HTMLConverter; import com.xpn.xwiki.web.Utils; /** * This filter is used to convert the values of request parameters that require HTML conversion before being processed. * A HTML editor can use this filter to convert its output to a specific syntax before it is saved. * * @version $Id: b6540ff6b850beca2f4d404bbcdc0f0b59fced5c $ */ public class ConversionFilter implements Filter { /** * The logger instance. */ private static final Logger LOGGER = LoggerFactory.getLogger(ConversionFilter.class); /** * The name of the request parameter whose multiple values indicate the request parameters that require HTML * conversion. For instance, if this parameter's value is {@code [description, content]} then the request has two * parameters, {@code description} and {@code content}, requiring HTML conversion. The syntax these parameters must * be converted to is found also on the request, under {@code description_syntax} and {@code content_syntax} * parameters. */ private static final String REQUIRES_HTML_CONVERSION = "RequiresHTMLConversion"; /** * The name of the session attribute holding the conversion output. The conversion output is stored in a {@link Map} * of {@link Map}s. The first key identifies the request and the second key is the name of the request parameter * that required HTML conversion. */ private static final String CONVERSION_OUTPUT = "com.xpn.xwiki.wysiwyg.server.converter.output"; /** * The name of the session attribute holding the conversion exceptions. The conversion exceptions are stored in a * {@link Map} of {@link Map}s. The first key identifies the request and the second key is the name of the request * parameter that required HTML conversion. */ private static final String CONVERSION_ERRORS = "com.xpn.xwiki.wysiwyg.server.converter.errors"; @Override public void destroy() { } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { // Take the list of request parameters that require HTML conversion. String[] parametersRequiringHTMLConversion = req.getParameterValues(REQUIRES_HTML_CONVERSION); if (parametersRequiringHTMLConversion != null) { MutableServletRequestFactory mreqFactory = Utils.getComponent((Type) MutableServletRequestFactory.class, req.getProtocol()); // Wrap the current request in order to be able to change request parameters. MutableServletRequest mreq = mreqFactory.newInstance(req); // Remove the list of request parameters that require HTML conversion to avoid recurrency. mreq.removeParameter(REQUIRES_HTML_CONVERSION); // Try to convert each parameter from the list and save caught exceptions. Map<String, Throwable> errors = new HashMap<String, Throwable>(); // Save also the output to prevent loosing data in case of conversion exceptions. Map<String, String> output = new HashMap<String, String>(); for (int i = 0; i < parametersRequiringHTMLConversion.length; i++) { String parameterName = parametersRequiringHTMLConversion[i]; String html = req.getParameter(parameterName); // Remove the syntax parameter from the request to avoid interference with further request processing. String syntax = mreq.removeParameter(parameterName + "_syntax"); if (html == null || syntax == null) { continue; } try { HTMLConverter converter = Utils.getComponent((Type) HTMLConverter.class); mreq.setParameter(parameterName, converter.fromHTML(html, syntax)); } catch (Exception e) { LOGGER.error(e.getLocalizedMessage(), e); errors.put(parameterName, e); } // If the conversion fails the output contains the value before the conversion. output.put(parameterName, mreq.getParameter(parameterName)); } if (!errors.isEmpty()) { handleConversionErrors(errors, output, mreq, res); } else { chain.doFilter(mreq, res); } } else { chain.doFilter(req, res); } } @Override public void init(FilterConfig config) throws ServletException { } private void handleConversionErrors(Map<String, Throwable> errors, Map<String, String> output, MutableServletRequest mreq, ServletResponse res) throws IOException { ServletRequest req = mreq.getRequest(); if (req instanceof HttpServletRequest && "XMLHttpRequest".equals(((HttpServletRequest) req).getHeader("X-Requested-With"))) { // If this is an AJAX request then we should simply send back the error. StringBuilder errorMessage = new StringBuilder(); // Aggregate all error messages (for all fields that have conversion errors). for (Map.Entry<String, Throwable> entry : errors.entrySet()) { errorMessage.append(entry.getKey()).append(": "); errorMessage.append(entry.getValue().getLocalizedMessage()).append('\n'); } ((HttpServletResponse) res).sendError(400, errorMessage.substring(0, errorMessage.length() - 1)); return; } // Otherwise, if this is a normal request, we have to redirect the request back and provide a key to // access the exception and the value before the conversion from the session. // Redirect to the error page specified on the request. String redirectURL = mreq.getParameter("xerror"); if (redirectURL == null) { // Redirect to the referrer page. redirectURL = mreq.getReferer(); } // Extract the query string. String queryString = StringUtils.substringAfterLast(redirectURL, String.valueOf('?')); // Remove the query string. redirectURL = StringUtils.substringBeforeLast(redirectURL, String.valueOf('?')); // Remove the previous key from the query string. We have to do this since this might not be the first // time the conversion fails for this redirect URL. queryString = queryString.replaceAll("key=.*&?", ""); if (queryString.length() > 0 && !queryString.endsWith(String.valueOf('&'))) { queryString += '&'; } // Save the output and the caught exceptions on the session. queryString += "key=" + save(mreq, output, errors); mreq.sendRedirect(res, redirectURL + '?' + queryString); } /** * Saves on the session the conversion output and the caught conversion exceptions, after a conversion failure. * * @param mreq the request used to access the session * @param output the conversion output for the given request * @param errors the conversion exceptions for the given request * @return a key that can be used along with the name of the request parameters that required HTML conversion to * extract the conversion output and the conversion exceptions from the {@link #CONVERSION_OUTPUT} and * {@value #CONVERSION_ERRORS} session attributes */ @SuppressWarnings("unchecked") private String save(MutableServletRequest mreq, Map<String, String> output, Map<String, Throwable> errors) { // Generate a random key to identify the request. String key = RandomStringUtils.randomAlphanumeric(4); // Save the output on the session. Map<String, Map<String, String>> conversionOutput = (Map<String, Map<String, String>>) mreq.getSessionAttribute(CONVERSION_OUTPUT); if (conversionOutput == null) { conversionOutput = new HashMap<String, Map<String, String>>(); mreq.setSessionAttribute(CONVERSION_OUTPUT, conversionOutput); } conversionOutput.put(key, output); // Save the errors on the session. Map<String, Map<String, Throwable>> conversionErrors = (Map<String, Map<String, Throwable>>) mreq.getSessionAttribute(CONVERSION_ERRORS); if (conversionErrors == null) { conversionErrors = new HashMap<String, Map<String, Throwable>>(); mreq.setSessionAttribute(CONVERSION_ERRORS, conversionErrors); } conversionErrors.put(key, errors); return key; } }