|  | @@ -19,6 +19,7 @@
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  package org.elasticsearch.rest;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +import org.elasticsearch.common.Booleans;
 | 
	
		
			
				|  |  |  import org.elasticsearch.common.Strings;
 | 
	
		
			
				|  |  |  import org.elasticsearch.common.path.PathTrie;
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -30,6 +31,11 @@ import java.util.regex.Pattern;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  public class RestUtils {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +     * Sets whether we decode a '+' in an url as a space or not.
 | 
	
		
			
				|  |  | +     */
 | 
	
		
			
				|  |  | +    private static final boolean DECODE_PLUS_AS_SPACE = Booleans.parseBoolean(System.getProperty("es.rest.url_plus_as_space", "false"));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      public static final PathTrie.Decoder REST_DECODER = new PathTrie.Decoder() {
 | 
	
		
			
				|  |  |          @Override
 | 
	
		
			
				|  |  |          public String decode(String value) {
 | 
	
	
		
			
				|  | @@ -55,7 +61,7 @@ public class RestUtils {
 | 
	
		
			
				|  |  |              c = s.charAt(i);
 | 
	
		
			
				|  |  |              if (c == '=' && name == null) {
 | 
	
		
			
				|  |  |                  if (pos != i) {
 | 
	
		
			
				|  |  | -                    name = decodeComponent(s.substring(pos, i));
 | 
	
		
			
				|  |  | +                    name = decodeQueryStringParam(s.substring(pos, i));
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  |                  pos = i + 1;
 | 
	
		
			
				|  |  |              } else if (c == '&' || c == ';') {
 | 
	
	
		
			
				|  | @@ -63,9 +69,9 @@ public class RestUtils {
 | 
	
		
			
				|  |  |                      // We haven't seen an `=' so far but moved forward.
 | 
	
		
			
				|  |  |                      // Must be a param of the form '&a&' so add it with
 | 
	
		
			
				|  |  |                      // an empty value.
 | 
	
		
			
				|  |  | -                    addParam(params, decodeComponent(s.substring(pos, i)), "");
 | 
	
		
			
				|  |  | +                    addParam(params, decodeQueryStringParam(s.substring(pos, i)), "");
 | 
	
		
			
				|  |  |                  } else if (name != null) {
 | 
	
		
			
				|  |  | -                    addParam(params, name, decodeComponent(s.substring(pos, i)));
 | 
	
		
			
				|  |  | +                    addParam(params, name, decodeQueryStringParam(s.substring(pos, i)));
 | 
	
		
			
				|  |  |                      name = null;
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  |                  pos = i + 1;
 | 
	
	
		
			
				|  | @@ -74,15 +80,19 @@ public class RestUtils {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          if (pos != i) {  // Are there characters we haven't dealt with?
 | 
	
		
			
				|  |  |              if (name == null) {     // Yes and we haven't seen any `='.
 | 
	
		
			
				|  |  | -                addParam(params, decodeComponent(s.substring(pos, i)), "");
 | 
	
		
			
				|  |  | +                addParam(params, decodeQueryStringParam(s.substring(pos, i)), "");
 | 
	
		
			
				|  |  |              } else {                // Yes and this must be the last value.
 | 
	
		
			
				|  |  | -                addParam(params, name, decodeComponent(s.substring(pos, i)));
 | 
	
		
			
				|  |  | +                addParam(params, name, decodeQueryStringParam(s.substring(pos, i)));
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |          } else if (name != null) {  // Have we seen a name without value?
 | 
	
		
			
				|  |  |              addParam(params, name, "");
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    private static String decodeQueryStringParam(final String s) {
 | 
	
		
			
				|  |  | +        return decodeComponent(s, StandardCharsets.UTF_8, true);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      private static void addParam(Map<String, String> params, String name, String value) {
 | 
	
		
			
				|  |  |          params.put(name, value);
 | 
	
		
			
				|  |  |      }
 | 
	
	
		
			
				|  | @@ -90,7 +100,7 @@ public class RestUtils {
 | 
	
		
			
				|  |  |      /**
 | 
	
		
			
				|  |  |       * Decodes a bit of an URL encoded by a browser.
 | 
	
		
			
				|  |  |       * <p>
 | 
	
		
			
				|  |  | -     * This is equivalent to calling {@link #decodeComponent(String, Charset)}
 | 
	
		
			
				|  |  | +     * This is equivalent to calling {@link #decodeComponent(String, Charset, boolean)}
 | 
	
		
			
				|  |  |       * with the UTF-8 charset (recommended to comply with RFC 3986, Section 2).
 | 
	
		
			
				|  |  |       *
 | 
	
		
			
				|  |  |       * @param s The string to decode (can be empty).
 | 
	
	
		
			
				|  | @@ -100,7 +110,7 @@ public class RestUtils {
 | 
	
		
			
				|  |  |       *                                  escape sequence.
 | 
	
		
			
				|  |  |       */
 | 
	
		
			
				|  |  |      public static String decodeComponent(final String s) {
 | 
	
		
			
				|  |  | -        return decodeComponent(s, StandardCharsets.UTF_8);
 | 
	
		
			
				|  |  | +        return decodeComponent(s, StandardCharsets.UTF_8, DECODE_PLUS_AS_SPACE);
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      /**
 | 
	
	
		
			
				|  | @@ -119,52 +129,50 @@ public class RestUtils {
 | 
	
		
			
				|  |  |       * Actually this function doesn't allocate any memory if there's nothing
 | 
	
		
			
				|  |  |       * to decode, the argument itself is returned.
 | 
	
		
			
				|  |  |       *
 | 
	
		
			
				|  |  | -     * @param s       The string to decode (can be empty).
 | 
	
		
			
				|  |  | -     * @param charset The charset to use to decode the string (should really
 | 
	
		
			
				|  |  | -     *                be {@link StandardCharsets#UTF_8}.
 | 
	
		
			
				|  |  | +     * @param s           The string to decode (can be empty).
 | 
	
		
			
				|  |  | +     * @param charset     The charset to use to decode the string (should really
 | 
	
		
			
				|  |  | +     *                    be {@link StandardCharsets#UTF_8}.
 | 
	
		
			
				|  |  | +     * @param plusAsSpace Whether to decode a {@code '+'} to a single space {@code ' '}
 | 
	
		
			
				|  |  |       * @return The decoded string, or {@code s} if there's nothing to decode.
 | 
	
		
			
				|  |  |       *         If the string to decode is {@code null}, returns an empty string.
 | 
	
		
			
				|  |  |       * @throws IllegalArgumentException if the string contains a malformed
 | 
	
		
			
				|  |  |       *                                  escape sequence.
 | 
	
		
			
				|  |  |       */
 | 
	
		
			
				|  |  | -    public static String decodeComponent(final String s, final Charset charset) {
 | 
	
		
			
				|  |  | +    private static String decodeComponent(final String s, final Charset charset, boolean plusAsSpace) {
 | 
	
		
			
				|  |  |          if (s == null) {
 | 
	
		
			
				|  |  |              return "";
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |          final int size = s.length();
 | 
	
		
			
				|  |  | -        if (!decodingNeeded(s, size)) {
 | 
	
		
			
				|  |  | +        if (!decodingNeeded(s, size, plusAsSpace)) {
 | 
	
		
			
				|  |  |              return s;
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |          final byte[] buf = new byte[size];
 | 
	
		
			
				|  |  | -        int pos = decode(s, size, buf);
 | 
	
		
			
				|  |  | +        int pos = decode(s, size, buf, plusAsSpace);
 | 
	
		
			
				|  |  |          return new String(buf, 0, pos, charset);
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    @SuppressWarnings("fallthrough")
 | 
	
		
			
				|  |  | -    private static boolean decodingNeeded(String s, int size) {
 | 
	
		
			
				|  |  | +    private static boolean decodingNeeded(String s, int size, boolean plusAsSpace) {
 | 
	
		
			
				|  |  |          boolean decodingNeeded = false;
 | 
	
		
			
				|  |  |          for (int i = 0; i < size; i++) {
 | 
	
		
			
				|  |  |              final char c = s.charAt(i);
 | 
	
		
			
				|  |  | -            switch (c) {
 | 
	
		
			
				|  |  | -                case '%':
 | 
	
		
			
				|  |  | -                    i++;  // We can skip at least one char, e.g. `%%'.
 | 
	
		
			
				|  |  | -                    // Fall through.
 | 
	
		
			
				|  |  | -                case '+':
 | 
	
		
			
				|  |  | -                    decodingNeeded = true;
 | 
	
		
			
				|  |  | -                    break;
 | 
	
		
			
				|  |  | +            if (c == '%') {
 | 
	
		
			
				|  |  | +                i++;  // We can skip at least one char, e.g. `%%'.
 | 
	
		
			
				|  |  | +                decodingNeeded = true;
 | 
	
		
			
				|  |  | +            } else if (plusAsSpace && c == '+') {
 | 
	
		
			
				|  |  | +                decodingNeeded = true;
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |          return decodingNeeded;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      @SuppressWarnings("fallthrough")
 | 
	
		
			
				|  |  | -    private static int decode(String s, int size, byte[] buf) {
 | 
	
		
			
				|  |  | +    private static int decode(String s, int size, byte[] buf, boolean plusAsSpace) {
 | 
	
		
			
				|  |  |          int pos = 0;  // position in `buf'.
 | 
	
		
			
				|  |  |          for (int i = 0; i < size; i++) {
 | 
	
		
			
				|  |  |              char c = s.charAt(i);
 | 
	
		
			
				|  |  |              switch (c) {
 | 
	
		
			
				|  |  |                  case '+':
 | 
	
		
			
				|  |  | -                    buf[pos++] = ' ';  // "+" -> " "
 | 
	
		
			
				|  |  | +                    buf[pos++] = (byte) (plusAsSpace ? ' ' : '+');  // "+" -> " "
 | 
	
		
			
				|  |  |                      break;
 | 
	
		
			
				|  |  |                  case '%':
 | 
	
		
			
				|  |  |                      if (i == size - 1) {
 |