/******************************************************************************* * Copyright 2015 Miami-Dade County * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package org.sharegov.cirm; import java.io.IOException; import java.net.URLDecoder; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import org.restlet.Context; import org.restlet.Request; import org.restlet.Response; import org.restlet.data.Form; import org.restlet.data.MediaType; import org.restlet.engine.application.EncodeRepresentation; import org.restlet.engine.header.ContentType; import org.restlet.representation.EmptyRepresentation; import org.restlet.representation.Representation; import org.restlet.routing.Filter; import org.sharegov.cirm.utils.ThreadLocalStopwatch; /** * Performs an additional decoding step for form encoded requests with double urlencoded form parameters.<br> * <br> * Temporary solution for the problem that 311Hub client javascript software sometimes double encodes values for form parameters to overcome a * bug in a previous Restlet version.<br> * <br> * This class assumes that all client request form parameters are UTF-8 encoded. * <br> * This filter must be used AFTER gzip decoding completed on a request. * <br> * A counter totalDecodings of actual decodings is available to track progress while web client code adopts.<br> * Once all double encoded form params are modified to normal encoding, this filter should be removed. * <br> * @author Thomas Hilpold * */ public class FormParamAdditionalDecodeFilter extends Filter { public static boolean DBG = false; public static final String DECODER_CHAR_SET = "UTF-8"; private static final AtomicInteger totalDecodings = new AtomicInteger(0); public FormParamAdditionalDecodeFilter(Context context) { super(context); } /** * Modifies the request entity iff it contains double encoded form parameter values. <br> * Requests without form parameters will be passed through efficiently. <br> * No response modification occurs.<br> */ @Override protected int beforeHandle(Request request, Response response) { try { decodeFormParamsIfDoubleEncoded(request); } catch (IOException e) { throw new IllegalStateException(e); } return CONTINUE; } /** * Decodes form parameters that are sent double encoded by performing one decode step on their values, if * their restlet framework decoded value starts with an "%". * * @param request a restlet request * @throws IOException did not occur during tests but may. * @throws IllegalArgumentException if an Encode representation is received. */ void decodeFormParamsIfDoubleEncoded(Request request) throws IOException { Representation r = request.getEntity(); if (r instanceof EncodeRepresentation) throw new IllegalArgumentException("Received an Encode representation." + " This filter must be after the Encoder filter. please check your filter chain order."); if (!(r instanceof EmptyRepresentation)) { ContentType c = new ContentType(r); if (MediaType.APPLICATION_WWW_FORM.equals(c.getMediaType(), true)) { Form form = new Form(r); Form newform = new Form(r); Map<String, String> valuesMap = form.getValuesMap(); for (Map.Entry<String, String> e : valuesMap.entrySet()) { if (DBG) ThreadLocalStopwatch.now("" + e.getKey() + " - " + e.getValue()); String shouldBeDecodedValue = e.getValue(); if (shouldBeDecodedValue.startsWith("%")) { shouldBeDecodedValue = URLDecoder.decode(e.getValue(), DECODER_CHAR_SET); totalDecodings.incrementAndGet(); if (DBG) { ThreadLocalStopwatch.now("DECODED " + request.getResourceRef()); ThreadLocalStopwatch.now("DECODED " + totalDecodings.get() + " : " + e.getKey() + " - " + shouldBeDecodedValue); } } newform.add(e.getKey(), shouldBeDecodedValue); } //we must always set the entity, because above getEntitiy call causes //NPEs later if repeated by the framework. request.setEntity(newform.encode(), c.getMediaType()); } } } /** * Returns the total count of actually needed decodings for double encoded form parameter values. * After fixing the client software this counter will remain at zero and this filter class can be removed. * Static for convenience. * * @return the total number of actual form value decodings, lower is better. */ public static int getTotalDecodings() { return totalDecodings.get(); } }