LCOV - code coverage report
Current view: top level - httpd/restapi - ParameterParser.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 88 109 80.7 %
Date: 2022-11-19 15:00:39 Functions: 9 11 81.8 %

          Line data    Source code
       1             : // Copyright (C) 2012 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.httpd.restapi;
      16             : 
      17             : import static com.google.gerrit.httpd.restapi.RestApiServlet.ALLOWED_CORS_METHODS;
      18             : import static com.google.gerrit.httpd.restapi.RestApiServlet.XD_AUTHORIZATION;
      19             : import static com.google.gerrit.httpd.restapi.RestApiServlet.XD_CONTENT_TYPE;
      20             : import static com.google.gerrit.httpd.restapi.RestApiServlet.XD_METHOD;
      21             : import static com.google.gerrit.httpd.restapi.RestApiServlet.replyBinaryResult;
      22             : import static com.google.gerrit.httpd.restapi.RestApiServlet.replyError;
      23             : import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
      24             : 
      25             : import com.google.auto.value.AutoValue;
      26             : import com.google.common.annotations.VisibleForTesting;
      27             : import com.google.common.base.Splitter;
      28             : import com.google.common.base.Strings;
      29             : import com.google.common.collect.ImmutableListMultimap;
      30             : import com.google.common.collect.ImmutableSet;
      31             : import com.google.common.collect.Iterables;
      32             : import com.google.common.collect.ListMultimap;
      33             : import com.google.common.collect.MultimapBuilder;
      34             : import com.google.gerrit.common.Nullable;
      35             : import com.google.gerrit.extensions.restapi.BadRequestException;
      36             : import com.google.gerrit.extensions.restapi.BinaryResult;
      37             : import com.google.gerrit.extensions.restapi.Url;
      38             : import com.google.gerrit.server.DynamicOptions;
      39             : import com.google.gerrit.util.cli.CmdLineParser;
      40             : import com.google.gerrit.util.http.CacheHeaders;
      41             : import com.google.gson.JsonArray;
      42             : import com.google.gson.JsonElement;
      43             : import com.google.gson.JsonObject;
      44             : import com.google.gson.JsonPrimitive;
      45             : import com.google.inject.Inject;
      46             : import com.google.inject.Singleton;
      47             : import java.io.IOException;
      48             : import java.io.StringWriter;
      49             : import java.util.HashSet;
      50             : import java.util.Iterator;
      51             : import java.util.Map;
      52             : import java.util.Set;
      53             : import javax.servlet.http.HttpServletRequest;
      54             : import javax.servlet.http.HttpServletResponse;
      55             : import org.kohsuke.args4j.CmdLineException;
      56             : 
      57             : public class ParameterParser {
      58             :   public static final String TRACE_PARAMETER = "trace";
      59             :   public static final String EXPERIMENT_PARAMETER = "experiment";
      60             : 
      61          31 :   private static final ImmutableSet<String> RESERVED_KEYS =
      62          31 :       ImmutableSet.of(
      63             :           "pp",
      64             :           "prettyPrint",
      65             :           "strict",
      66             :           "callback",
      67             :           "alt",
      68             :           "fields",
      69             :           TRACE_PARAMETER,
      70             :           EXPERIMENT_PARAMETER);
      71             : 
      72             :   @AutoValue
      73          31 :   public abstract static class QueryParams {
      74          31 :     static final String I = QueryParams.class.getName();
      75             : 
      76             :     static QueryParams create(
      77             :         @Nullable String accessToken,
      78             :         @Nullable String xdMethod,
      79             :         @Nullable String xdContentType,
      80             :         ImmutableListMultimap<String, String> config,
      81             :         ImmutableListMultimap<String, String> params) {
      82          31 :       return new AutoValue_ParameterParser_QueryParams(
      83             :           accessToken, xdMethod, xdContentType, config, params);
      84             :     }
      85             : 
      86             :     @Nullable
      87             :     public abstract String accessToken();
      88             : 
      89             :     @Nullable
      90             :     abstract String xdMethod();
      91             : 
      92             :     @Nullable
      93             :     abstract String xdContentType();
      94             : 
      95             :     abstract ImmutableListMultimap<String, String> config();
      96             : 
      97             :     abstract ImmutableListMultimap<String, String> params();
      98             : 
      99             :     boolean hasXdOverride() {
     100          29 :       return xdMethod() != null || xdContentType() != null;
     101             :     }
     102             :   }
     103             : 
     104             :   public static QueryParams getQueryParams(HttpServletRequest req) throws BadRequestException {
     105          31 :     QueryParams qp = (QueryParams) req.getAttribute(QueryParams.I);
     106          31 :     if (qp != null) {
     107          28 :       return qp;
     108             :     }
     109             : 
     110          31 :     String accessToken = null;
     111          31 :     String xdMethod = null;
     112          31 :     String xdContentType = null;
     113          31 :     ListMultimap<String, String> config = MultimapBuilder.hashKeys(4).arrayListValues().build();
     114          31 :     ListMultimap<String, String> params = MultimapBuilder.hashKeys().arrayListValues().build();
     115             : 
     116          31 :     String queryString = req.getQueryString();
     117          31 :     if (!Strings.isNullOrEmpty(queryString)) {
     118          13 :       for (String kvPair : Splitter.on('&').split(queryString)) {
     119          13 :         Iterator<String> i = Splitter.on('=').limit(2).split(kvPair).iterator();
     120          13 :         String key = Url.decode(i.next());
     121          13 :         String val = i.hasNext() ? Url.decode(i.next()) : "";
     122             : 
     123          13 :         if (XD_AUTHORIZATION.equals(key)) {
     124           1 :           if (accessToken != null) {
     125           0 :             throw new BadRequestException("duplicate " + XD_AUTHORIZATION);
     126             :           }
     127           1 :           accessToken = val;
     128          13 :         } else if (XD_METHOD.equals(key)) {
     129           2 :           if (xdMethod != null) {
     130           1 :             throw new BadRequestException("duplicate " + XD_METHOD);
     131           2 :           } else if (!ALLOWED_CORS_METHODS.contains(val)) {
     132           1 :             throw new BadRequestException("invalid " + XD_METHOD);
     133             :           }
     134           2 :           xdMethod = val;
     135          13 :         } else if (XD_CONTENT_TYPE.equals(key)) {
     136           2 :           if (xdContentType != null) {
     137           1 :             throw new BadRequestException("duplicate " + XD_CONTENT_TYPE);
     138             :           }
     139           2 :           xdContentType = val;
     140          13 :         } else if (RESERVED_KEYS.contains(key)) {
     141           1 :           config.put(key, val);
     142             :         } else {
     143          13 :           params.put(key, val);
     144             :         }
     145          13 :       }
     146             :     }
     147             : 
     148          31 :     qp =
     149          31 :         QueryParams.create(
     150             :             accessToken,
     151             :             xdMethod,
     152             :             xdContentType,
     153          31 :             ImmutableListMultimap.copyOf(config),
     154          31 :             ImmutableListMultimap.copyOf(params));
     155          31 :     req.setAttribute(QueryParams.I, qp);
     156          31 :     return qp;
     157             :   }
     158             : 
     159             :   private final CmdLineParser.Factory parserFactory;
     160             : 
     161             :   @Inject
     162          27 :   ParameterParser(CmdLineParser.Factory pf) {
     163          27 :     this.parserFactory = pf;
     164          27 :   }
     165             : 
     166             :   /**
     167             :    * Parses query parameters ({@code in}) into annotated option fields of {@code param}.
     168             :    *
     169             :    * @return true if parsing was successful. Requesting help is considered failure and returns
     170             :    *     false.
     171             :    */
     172             :   <T> boolean parse(
     173             :       T param,
     174             :       DynamicOptions pluginOptions,
     175             :       ListMultimap<String, String> in,
     176             :       HttpServletRequest req,
     177             :       HttpServletResponse res)
     178             :       throws IOException {
     179          27 :     if (param.getClass().getAnnotation(Singleton.class) != null) {
     180             :       // Command-line parsing mutates the object, so we can't have options on @Singleton.
     181          21 :       return true;
     182             :     }
     183          16 :     CmdLineParser clp = parserFactory.create(param);
     184          16 :     pluginOptions.setBean(param);
     185          16 :     pluginOptions.startLifecycleListeners();
     186          16 :     pluginOptions.parseDynamicBeans(clp);
     187          16 :     pluginOptions.setDynamicBeans();
     188          16 :     pluginOptions.onBeanParseStart();
     189             :     try {
     190          15 :       clp.parseOptionMap(in);
     191           4 :     } catch (CmdLineException | NumberFormatException e) {
     192           4 :       if (!clp.wasHelpRequestedByOption()) {
     193           4 :         replyError(req, res, SC_BAD_REQUEST, e.getMessage(), e);
     194           4 :         return false;
     195             :       }
     196          15 :     }
     197             : 
     198          15 :     if (clp.wasHelpRequestedByOption()) {
     199           0 :       StringWriter msg = new StringWriter();
     200           0 :       clp.printQueryStringUsage(req.getRequestURI(), msg);
     201           0 :       msg.write('\n');
     202           0 :       msg.write('\n');
     203           0 :       clp.printUsage(msg, null);
     204           0 :       msg.write('\n');
     205           0 :       CacheHeaders.setNotCacheable(res);
     206           0 :       replyBinaryResult(req, res, BinaryResult.create(msg.toString()).setContentType("text/plain"));
     207           0 :       return false;
     208             :     }
     209          15 :     pluginOptions.onBeanParseEnd();
     210             : 
     211          15 :     return true;
     212             :   }
     213             : 
     214             :   private static Set<String> query(HttpServletRequest req) {
     215           0 :     Set<String> params = new HashSet<>();
     216           0 :     if (!Strings.isNullOrEmpty(req.getQueryString())) {
     217           0 :       for (String kvPair : Splitter.on('&').split(req.getQueryString())) {
     218           0 :         params.add(Iterables.getFirst(Splitter.on('=').limit(2).split(kvPair), null));
     219           0 :       }
     220             :     }
     221           0 :     return params;
     222             :   }
     223             : 
     224             :   /**
     225             :    * Convert a standard URL encoded form input into a parsed JSON tree.
     226             :    *
     227             :    * <p>Given an input such as:
     228             :    *
     229             :    * <pre>
     230             :    * message=Does+not+compile.&labels.Verified=-1
     231             :    * </pre>
     232             :    *
     233             :    * which is easily created using the curl command line tool:
     234             :    *
     235             :    * <pre>
     236             :    * curl --data 'message=Does not compile.' --data labels.Verified=-1
     237             :    * </pre>
     238             :    *
     239             :    * converts to a JSON object structure that is normally expected:
     240             :    *
     241             :    * <pre>
     242             :    * {
     243             :    *   "message": "Does not compile.",
     244             :    *   "labels": {
     245             :    *     "Verified": "-1"
     246             :    *   }
     247             :    * }
     248             :    * </pre>
     249             :    *
     250             :    * This input can then be further processed into the Java input type expected by a view using
     251             :    * Gson. Here we rely on Gson to perform implicit conversion of a string {@code "-1"} to a number
     252             :    * type when the Java input type expects a number.
     253             :    *
     254             :    * <p>Conversion assumes any field name that does not contain {@code "."} will be a property of
     255             :    * the top level input object. Any field with a dot will use the first segment as the top level
     256             :    * property name naming an object, and the rest of the field name as a property in the nested
     257             :    * object.
     258             :    *
     259             :    * @param req request to parse form input from and create JSON tree.
     260             :    * @return the converted JSON object tree.
     261             :    * @throws BadRequestException the request cannot be cast, as there are conflicting definitions
     262             :    *     for a nested object.
     263             :    */
     264             :   static JsonObject formToJson(HttpServletRequest req) throws BadRequestException {
     265           0 :     Map<String, String[]> map = req.getParameterMap();
     266           0 :     return formToJson(map, query(req));
     267             :   }
     268             : 
     269             :   @VisibleForTesting
     270             :   static JsonObject formToJson(Map<String, String[]> map, Set<String> query)
     271             :       throws BadRequestException {
     272           1 :     JsonObject inputObject = new JsonObject();
     273           1 :     for (Map.Entry<String, String[]> ent : map.entrySet()) {
     274           1 :       String key = ent.getKey();
     275           1 :       String[] values = ent.getValue();
     276             : 
     277           1 :       if (query.contains(key) || values.length == 0) {
     278             :         // Disallow processing query parameters as input body fields.
     279             :         // Implementations of views should avoid duplicate naming.
     280           0 :         continue;
     281             :       }
     282             : 
     283           1 :       JsonObject obj = inputObject;
     284           1 :       int dot = key.indexOf('.');
     285           1 :       if (0 <= dot) {
     286           1 :         String property = key.substring(0, dot);
     287           1 :         JsonElement e = inputObject.get(property);
     288           1 :         if (e == null) {
     289           1 :           obj = new JsonObject();
     290           1 :           inputObject.add(property, obj);
     291           1 :         } else if (e.isJsonObject()) {
     292           1 :           obj = e.getAsJsonObject();
     293             :         } else {
     294           0 :           throw new BadRequestException(String.format("key %s conflicts with %s", key, property));
     295             :         }
     296           1 :         key = key.substring(dot + 1);
     297             :       }
     298             : 
     299           1 :       if (obj.get(key) != null) {
     300             :         // This error should never happen. If all form values are handled
     301             :         // together in a single pass properties are set only once. Setting
     302             :         // again indicates something has gone very wrong.
     303           0 :         throw new BadRequestException("invalid form input, use JSON instead");
     304           1 :       } else if (values.length == 1) {
     305           1 :         obj.addProperty(key, values[0]);
     306             :       } else {
     307           1 :         JsonArray list = new JsonArray();
     308           1 :         for (String v : values) {
     309           1 :           list.add(new JsonPrimitive(v));
     310             :         }
     311           1 :         obj.add(key, list);
     312             :       }
     313           1 :     }
     314           1 :     return inputObject;
     315             :   }
     316             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750