/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */


#include "net/instaweb/rewriter/public/rewrite_query.h"

#include <algorithm>  // for std::binary_search
#include <map>
#include <utility>
#include <vector>

#include "base/logging.h"
#include "net/instaweb/http/public/request_context.h"
#include "net/instaweb/rewriter/public/image_rewrite_filter.h"
#include "net/instaweb/rewriter/public/request_properties.h"
#include "net/instaweb/rewriter/public/resource_namer.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_driver_factory.h"
#include "net/instaweb/rewriter/public/rewrite_filter.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/server_context.h"
#include "pagespeed/kernel/base/message_handler.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_multi_map.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/http/google_url.h"
#include "pagespeed/kernel/http/http_names.h"
#include "pagespeed/kernel/http/query_params.h"
#include "pagespeed/kernel/http/response_headers.h"

namespace net_instaweb {

namespace {

// We use + and = inside the resource-options URL segment because they will not
// be quoted by UrlEscaper, unlike "," and ":".
const char kResourceFilterSeparator[] = "+";
const char kResourceOptionValueSeparator[] = "=";

const char kProxyOptionSeparator[] = ",";
const char kProxyOptionValueSeparator = '=';
const char kProxyOptionVersion[] = "v";
const char kProxyOptionMode[] = "m";
const char kProxyOptionImageQualityPreference[] = "iqp";
const char kProxyOptionValidVersionValue[] = "1";

StringPiece SanitizeValueAsQP(StringPiece untrusted_value,
                              GoogleUrl* storage) {
  // This is ever so slightly hacky: we dummy up an URL with a QP where the
  // value of the QP is the untrusted value, then we discard everything
  // prior to the possibly-modified value in the resulting GoogleUrl.
  const char kUrlBase[] = "http://www.example.com/?x=";
  storage->Reset(StrCat(kUrlBase, untrusted_value));
  StringPiece sanitized_value = storage->Spec();
  return StringPiece(sanitized_value.data() + STATIC_STRLEN(kUrlBase),
                     sanitized_value.size() - STATIC_STRLEN(kUrlBase));
}

}  // namespace

const char RewriteQuery::kModPagespeed[] = "ModPagespeed";
const char RewriteQuery::kPageSpeed[] = "PageSpeed";

const char RewriteQuery::kModPagespeedFilters[] = "ModPagespeedFilters";
const char RewriteQuery::kPageSpeedFilters[] = "PageSpeedFilters";

const char RewriteQuery::kNoscriptValue[] = "noscript";

template <class HeaderT>
RewriteQuery::Status RewriteQuery::ScanHeader(
    bool allow_options,
    const GoogleString& request_option_override,
    const RequestContextPtr& request_context,
    HeaderT* headers,
    RequestProperties* request_properties,
    RewriteOptions* options,
    MessageHandler* handler) {
  Status status = kNoneFound;

  if (headers == NULL) {
    return status;
  }

  // Check to see if the override token exists.
  if (!allow_options && !request_option_override.empty()) {
    GoogleString mod_pagespeed_override =
        StrCat(kModPagespeed, RewriteOptions::kRequestOptionOverride);
    GoogleString page_speed_override =
        StrCat(kPageSpeed, RewriteOptions::kRequestOptionOverride);
    for (int i = 0, n = headers->NumAttributes(); i < n; ++i) {
      const StringPiece name(headers->Name(i));
      const GoogleString& value = headers->Value(i);
      if (name == mod_pagespeed_override || name == page_speed_override) {
        allow_options = (value == request_option_override);
        break;
      }
    }
  }

  // Tracks the headers that need to be removed.
  // It doesn't matter what type of headers we use, so we use RequestHeaders.
  RequestHeaders headers_to_remove;

  for (int i = 0, n = headers->NumAttributes(); i < n; ++i) {
    const StringPiece name(headers->Name(i));
    const GoogleString& value = headers->Value(i);
    switch (ScanNameValue(name, value, allow_options, request_context,
                          request_properties, options, handler)) {
      case kNoneFound:
        break;
      case kSuccess:
        if (name.starts_with(kModPagespeed) || name.starts_with(kPageSpeed)) {
          headers_to_remove.Add(name, value);
        }
        status = kSuccess;
        break;
      case kInvalid:
        return kInvalid;
    }
  }

  // TODO(bolian): jmarantz suggested below change.  we should make a
  // StringSetInsensitive and put all the names we want to remove including
  // XPSAClientOptions and then call RemoveAllFromSet.
  // That will be more efficient.
  for (int i = 0, n = headers_to_remove.NumAttributes(); i < n; ++i) {
    headers->Remove(headers_to_remove.Name(i), headers_to_remove.Value(i));
  }
  // kXPsaClientOptions is meant for proxy only. Remove it in any case.
  headers->RemoveAll(HttpAttributes::kXPsaClientOptions);

  return status;
}

RewriteQuery::RewriteQuery() {
}

RewriteQuery::~RewriteQuery() {
}

// Scan for option-sets in cookies, query params, request and response headers.
// We only allow a limited number of options to be set. In particular, some
// options are risky to set this way, such as image inline threshold, which
// exposes a DOS vulnerability and a risk of poisoning our internal cache, and
// domain adjustments, which can introduce a security vulnerability.
RewriteQuery::Status RewriteQuery::Scan(
    bool allow_related_options,
    bool allow_options_to_be_specified_by_cookies,
    const GoogleString& request_option_override,
    const RequestContextPtr& request_context,
    RewriteDriverFactory* factory,
    ServerContext* server_context,
    GoogleUrl* request_url,
    RequestHeaders* request_headers,
    ResponseHeaders* response_headers,
    MessageHandler* handler) {
  Status status = kNoneFound;
  query_params_.Clear();
  pagespeed_query_params_.Clear();
  pagespeed_option_cookies_.Clear();
  options_.reset(NULL);

  // To support serving resources from servers that don't share the
  // same settings as the ones generating HTML, we can put whitelisted
  // option-settings into the query-params by ID.  But we expose this
  // setting (a) only for .pagespeed. resources, not HTML, and (b)
  // only when allow_related_options is true.
  ResourceNamer namer;

  bool return_after_parsing = false;
  if (allow_related_options &&
      namer.DecodeIgnoreHashAndSignature(request_url->LeafSansQuery()) &&
      namer.has_options()) {
    const RewriteFilter* rewrite_filter =
        server_context->FindFilterForDecoding(namer.id());
    if (rewrite_filter != NULL) {
      options_.reset(factory->NewRewriteOptionsForQuery());
      status = ParseResourceOption(namer.options(), options_.get(),
                                   rewrite_filter);
      if (status != kSuccess) {
        options_.reset(NULL);

        // We want query_params() to be populated after calling
        // RewriteQuery::Scan, even if any URL-embedded configuration
        // parameters are invalid.  So we delay our early exit until
        // after the query_params_.Parse call below.
        return_after_parsing = true;
      }
    }
  }

  // Extract all cookies iff we can use them to set options.
  RequestHeaders::CookieMultimap no_cookies;
  const RequestHeaders::CookieMultimap& all_cookies(
      (allow_options_to_be_specified_by_cookies && request_headers != NULL)
      ? request_headers->GetAllCookies()
      : no_cookies);

  // For XmlHttpRequests, disable filters that can't run in Ajax.
  //
  // For example, for filters that insert scripts, there will be two
  // copies of the same scripts in the html dom -- one from main html
  // page and another from html content fetched from ajax --- which
  // will generally confuse the heck out of it. The code for this is a
  // little special since unlike a PageSpeedFoo= header we should not
  // take it as an invitation to turn stuff on.
  //
  // Another commmon case is filters that require a <head> element -- we
  // don't want to add a <head> in the ajax response, so we can't run
  // any filters that might depend on that.
  //
  // TODO(sriharis): Set a flag in RewriteOptions indicating that we are
  // working with Ajax and thus should not assume the base URL is correct.
  // Note that there is no guarantee that the header will be set on an ajax
  // request and so the option will not be set for all ajax requests.
  //
  // TODO(jmarantz): Ajax status should probably be in RequestProperties
  // rather than RewriteOptions so we don't have to create a new driver
  // on an ajax request, but can instead reference properties.  However
  // that change may be fairly significant.
  if (request_headers != NULL && request_headers->IsXmlHttpRequest()) {
    if (options_.get() == NULL) {
      options_.reset(factory->NewRewriteOptionsForQuery());
    }
    options_->DisableFiltersThatCantRunInAjax();
    status = kSuccess;
  }

  // See if anything looks even remotely like one of our options before doing
  // any more work.  Note that when options are correctly embedded in the URL,
  // we will have a success-status here.  But we still allow a hand-added
  // query-param to override the embedded options.
  query_params_.ParseFromUrl(*request_url);
  if (return_after_parsing ||
      !MayHaveCustomOptions(query_params_, request_headers, response_headers,
                            all_cookies)) {
    return status;
  }

  if (options_.get() == NULL) {
    options_.reset(factory->NewRewriteOptionsForQuery());
  }

  scoped_ptr<RequestProperties> request_properties;
  if (request_headers != NULL) {
    request_properties.reset(server_context->NewRequestProperties());
    request_properties->SetUserAgent(
        request_headers->Lookup1(HttpAttributes::kUserAgent));
  }

  // Check to see if options should be parsed.
  // If the config disallows parsing, and the proper token is not provided,
  // do not use the options passed in the url.
  bool allow_options = true;
  if (!request_option_override.empty()) {
    allow_options = false;
    GoogleString override_token;
    GoogleString mod_pagespeed_override =
        StrCat(kModPagespeed, RewriteOptions::kRequestOptionOverride);
    GoogleString page_speed_override =
        StrCat(kPageSpeed, RewriteOptions::kRequestOptionOverride);
    if (query_params_.Lookup1Unescaped(mod_pagespeed_override,
                                       &override_token) ||
        query_params_.Lookup1Unescaped(page_speed_override, &override_token)) {
      allow_options = (override_token == request_option_override);
    }
  }

  // Scan for options set as cookies. They can be overridden by QPs or headers.
  // An explanation of the life cycle of a PageSpeed option cookie:
  // * Initially the value is passed in as a GoogleUrl query parameter.
  // * GoogleUrl does minimal escaping, mainly removing whitespace and
  //   percent-encoding control characters.
  // * That is parsed by QueryParams (above), which does no further escaping.
  // * Since cookie values have restrictions on the characters allowed in the
  //   value (e.g. no ';'s), we GoogleUrl::Escape the value, which does
  //   significant percent-escaping (nearly everything except alphanumeric).
  //   This is done in ResponseHeaders::SetPageSpeedQueryParamsAsCookies().
  // [So now we're here, where we use the cookie values set by the above steps]
  // * We unescape the cookie value to reverse the previous step.
  //   Note that GoogleUrl::UnescapeQueryParam(GoogleUrl::EscapeQueryParam(x))
  //   is the identify function, so we expect the value to be GoogleUrl
  //   minimally escaped.
  // TODO(sligocki): GoogleUrl::UnescapeIgnorePlus(GoogleUrl::EscapeQueryParam)
  //                 is not the identity function. Is this a problem?
  // * We sanitize the unescaped cookie value by dummying up a GoogleUrl with
  //   the value as a query parameter value, hence re-minimally escaping it.
  // * We then escape this sanitized value since that's the process that we
  //   went through above: if this escaped value equals the cookie value then
  //   the cookie value seems authentic -and- the unescaped value must also be
  //   sanitized, meaning it's safe to pass to our value parsing logic. If the
  //   escaped value does -not- equal the cookie value, it means the cookie has
  //   characters that we cannot have put there, and we assume that someone has
  //   manually set it in an attempt to circumvent our precautions, so we
  //   ignore the cookie completely.
  RequestHeaders::CookieMultimapConstIter it, end;
  for (it = all_cookies.begin(), end = all_cookies.end(); it != end; ++it) {
    StringPiece cookie_name = it->first;
    if (MightBeCustomOption(cookie_name)) {
      GoogleUrl gurl;
      StringPiece cookie_value = it->second.first;
      GoogleString unescaped = GoogleUrl::UnescapeIgnorePlus(cookie_value);
      StringPiece sanitized = SanitizeValueAsQP(unescaped, &gurl);
      GoogleString escaped = GoogleUrl::EscapeQueryParam(sanitized);
      if (unescaped == sanitized && escaped == cookie_value) {
        RequestContextPtr null_request_context;
        if (ScanNameValue(cookie_name, unescaped, allow_options,
                          null_request_context, request_properties.get(),
                          options_.get(), handler) == kSuccess) {
          pagespeed_option_cookies_.AddEscaped(cookie_name, unescaped);
          status = kSuccess;
        }
      } else {
        // PageSpeed cookies with an invalid value will not be cleared. This
        // is unfortunate but OK since they'll never apply (due to the invalid
        // value) and will eventually expire anyway.
        handler->Message(kInfo, "PageSpeed Cookie value seems mangled: "
                         "name='%s', value='%s', escaped value='%s'",
                         cookie_name.as_string().c_str(),
                         cookie_value.as_string().c_str(),
                         escaped.c_str());
      }
    }
  }

  pagespeed_query_params_.Clear();
  QueryParams temp_query_params;
  for (int i = 0; i < query_params_.size(); ++i) {
    GoogleString unescaped_value;
    if (query_params_.UnescapedValue(i, &unescaped_value)) {
      // The Unescaper changes "+" to " ", which is not what we want, and
      // is not what happens for response headers and request headers, so
      // let's fix it now.
      GlobalReplaceSubstring(" " , "+", &unescaped_value);
      switch (ScanNameValue(
          query_params_.name(i), unescaped_value, allow_options,
          request_context, request_properties.get(), options_.get(), handler)) {
        case kNoneFound:
          // If this is not a PageSpeed-related query-parameter, then save it
          // in its escaped form.
          temp_query_params.AddEscaped(query_params_.name(i),
                                       *query_params_.EscapedValue(i));
          break;
        case kSuccess:
          // If it is a PageSpeed-related query parameter, also save it so we
          // can add it back if we receive a redirection response to our fetch.
          pagespeed_query_params_.AddEscaped(query_params_.name(i),
                                             *query_params_.EscapedValue(i));
          status = kSuccess;
          break;
        case kInvalid:
          status = kInvalid;
          options_.reset(NULL);
          return status;
      }
    } else {
      temp_query_params.AddEscaped(query_params_.name(i), StringPiece());
    }
  }
  if (status == kSuccess) {
    // Remove the ModPagespeed* or PageSpeed* for url.
    GoogleString temp_params = temp_query_params.empty() ? "" :
        StrCat("?", temp_query_params.ToEscapedString());
    request_url->Reset(StrCat(request_url->AllExceptQuery(), temp_params,
                              request_url->AllAfterQuery()));
  }

  switch (ScanHeader<RequestHeaders>(
      allow_options, request_option_override, request_context, request_headers,
      request_properties.get(), options_.get(), handler)) {
    case kNoneFound:
      break;
    case kSuccess:
      status = kSuccess;
      break;
    case kInvalid:
      status = kInvalid;
      options_.reset(NULL);
      return status;
  }

  switch (ScanHeader<ResponseHeaders>(
      allow_options, request_option_override, request_context, response_headers,
      request_properties.get(), options_.get(), handler)) {
    case kNoneFound:
      break;
    case kSuccess:
      status = kSuccess;
      break;
    case kInvalid:
      status = kInvalid;
      options_.reset(NULL);
      return status;
  }

  // Set a default rewrite level in case the mod_pagespeed server has no
  // rewriting options configured.
  // Note that if any filters are explicitly set with
  // PageSpeedFilters=..., then the call to
  // DisableAllFiltersNotExplicitlyEnabled() below will make the 'level'
  // irrelevant.
  switch (status) {
    case kSuccess:
      options_->SetDefaultRewriteLevel(RewriteOptions::kCoreFilters);
      break;
    case kNoneFound:
      options_.reset(NULL);
      break;
    case kInvalid:
      LOG(DFATAL) << "Invalid responses always use early exit";
      options_.reset(NULL);
      break;
  }
  return status;
}

bool RewriteQuery::MightBeCustomOption(StringPiece name) {
  // TODO(jmarantz): switch to case-insenstive comparisons for these prefixes.
  return name.starts_with(kModPagespeed) || name.starts_with(kPageSpeed) ||
      StringCaseEqual(name, HttpAttributes::kXPsaClientOptions);
}

template <class HeaderT>
bool RewriteQuery::HeadersMayHaveCustomOptions(const QueryParams& params,
                                               const HeaderT* headers) {
  if (headers != NULL) {
    for (int i = 0, n = headers->NumAttributes(); i < n; ++i) {
      if (MightBeCustomOption(headers->Name(i))) {
        return true;
      }
    }
  }
  return false;
}

bool RewriteQuery::CookiesMayHaveCustomOptions(
    const RequestHeaders::CookieMultimap& cookies) {
  RequestHeaders::CookieMultimapConstIter it = cookies.begin();
  RequestHeaders::CookieMultimapConstIter end = cookies.end();
  for (; it != end; ++it) {
    if (MightBeCustomOption(it->first)) {
      return true;
    }
  }
  return false;
}

bool RewriteQuery::MayHaveCustomOptions(
    const QueryParams& params, const RequestHeaders* req_headers,
    const ResponseHeaders* resp_headers,
    const RequestHeaders::CookieMultimap& cookies) {
  for (int i = 0, n = params.size(); i < n; ++i) {
    if (MightBeCustomOption(params.name(i))) {
      return true;
    }
  }
  if (HeadersMayHaveCustomOptions(params, req_headers)) {
    return true;
  }
  if (HeadersMayHaveCustomOptions(params, resp_headers)) {
    return true;
  }
  if (CookiesMayHaveCustomOptions(cookies)) {
    return true;
  }
  if (req_headers != NULL &&
      (req_headers->Has(HttpAttributes::kXPsaClientOptions) ||
       req_headers->HasValue(HttpAttributes::kCacheControl, "no-transform"))) {
    return true;
  }
  if ((resp_headers != NULL) && resp_headers->HasValue(
          HttpAttributes::kCacheControl, "no-transform")) {
    return true;
  }
  return false;
}

RewriteQuery::Status RewriteQuery::ScanNameValue(
    const StringPiece& name, const StringPiece& value, bool allow_options,
    const RequestContextPtr& request_context,
    RequestProperties* request_properties, RewriteOptions* options,
    MessageHandler* handler) {
  Status status = kNoneFound;

  // See https://github.com/apache/incubator-pagespeed-mod/issues/627
  // Evidently bots and other clients may not properly resolve the quoted
  // URLs we send into noscript links, so remove any excess quoting we
  // see around the value.
  StringPiece trimmed_value(value);
  TrimUrlQuotes(&trimmed_value);
  if (name == kModPagespeed || name == kPageSpeed) {
    RewriteOptions::EnabledEnum enabled;
    if (RewriteOptions::ParseFromString(trimmed_value, &enabled)) {
      options->set_enabled(enabled);
      status = kSuccess;
    } else if (trimmed_value.starts_with(kNoscriptValue)) {
      // We use starts_with("noscript") to help resolve Issue 874.
      // Disable filters that depend on custom script execution.
      options->DisableFiltersRequiringScriptExecution();
      options->EnableFilter(RewriteOptions::kHandleNoscriptRedirect);
      status = kSuccess;
    } else {
      // TODO(sligocki): Return 404s instead of logging server errors here
      // and below.
      handler->Message(kWarning, "Invalid value for %s: %s "
                       "(should be on, off, unplugged, or noscript)",
                       name.as_string().c_str(),
                       trimmed_value.as_string().c_str());
      status = kInvalid;
    }
  } else if (!allow_options) {
    status = kNoneFound;
  } else if (name == kModPagespeedFilters || name == kPageSpeedFilters) {
    // When using PageSpeedFilters query param, only the specified filters
    // should be enabled.
    if (options->AdjustFiltersByCommaSeparatedList(trimmed_value, handler)) {
      status = kSuccess;
    } else {
      status = kInvalid;
    }
  } else if (StringCaseEqual(name, HttpAttributes::kXPsaClientOptions)) {
    if (UpdateRewriteOptionsWithClientOptions(
        trimmed_value, request_properties, options)) {
      status = kSuccess;
    }
    // We don't want to return kInvalid, which causes 405 (kMethodNotAllowed)
    // returned to client.
  } else if (StringCaseEqual(name, HttpAttributes::kCacheControl)) {
    StringPieceVector pairs;
    SplitStringPieceToVector(trimmed_value, ",", &pairs,
                             true /* omit_empty_strings */);
    for (int i = 0, n = pairs.size(); i < n; ++i) {
      TrimWhitespace(&pairs[i]);
      if (pairs[i] == "no-transform") {
        // TODO(jmarantz): A .pagespeed resource should return un-optimized
        // content with "Cache-Control: no-transform".
        options->set_enabled(RewriteOptions::kEnabledStandby);
        status = kSuccess;
        break;
      }
    }
  } else if (name.starts_with(kModPagespeed) || name.starts_with(kPageSpeed)) {
    // Remove the initial ModPagespeed or PageSpeed.
    StringPiece name_suffix = name;
    stringpiece_ssize_type prefix_len;
    if (name.starts_with(kModPagespeed)) {
      prefix_len = sizeof(kModPagespeed)-1;
    } else {
      prefix_len = sizeof(kPageSpeed)-1;
    }
    name_suffix.remove_prefix(prefix_len);
    switch (options->SetOptionFromQuery(name_suffix, trimmed_value)) {
      case RewriteOptions::kOptionOk:
        status = kSuccess;
        break;
      case RewriteOptions::kOptionNameUnknown:
        if (request_context.get() != NULL &&
            StringCaseEqual(name_suffix,
                            RewriteOptions::kStickyQueryParameters)) {
          request_context->set_sticky_query_parameters_token(trimmed_value);
          status = kSuccess;
        } else {
          status = kNoneFound;
        }
        break;
      case RewriteOptions::kOptionValueInvalid:
        status = kInvalid;
        break;
    }
  }
  return status;
}

// In some environments it is desirable to bind a URL to the options
// that affect it.  One example of where this would be needed is if
// images are served by a separate cluster that doesn't share the same
// configuration as the mod_pagespeed instances that rewrote the HTML.
// In this case, we must encode the relevant options as query-params
// to be appended to the URL.  These should be decodable by Scan()
// above, though they don't need to be in the same verbose format that
// we document for debugging and experimentation.  They can use the
// more concise abbreviations of 2-4 letters for each option.
GoogleString RewriteQuery::GenerateResourceOption(
    StringPiece filter_id, RewriteDriver* driver) {
  const RewriteFilter* filter = driver->FindFilter(filter_id);
  // TODO(sligocki): We do not seem to be detecting Apache crashes in the
  // system_test. We should detect and fail when these crashes occur.
  CHECK(filter != NULL)
      << "Filter ID " << filter_id << " is not registered in RewriteDriver. "
      << "You must register it with a call to RegisterRewriteFilter() in "
      << "RewriteDriver::SetServerContext().";
  StringPiece prefix("");
  GoogleString value;
  const RewriteOptions* options = driver->options();

  // All the filters & options will be encoded into the value of a
  // single query param with name kAddQueryFromOptionName ("PsolOpt").
  // The value will have the comma-separated filters IDs, and option IDs,
  // which are all given a 2-4 letter codes.  The only difference between
  // options & filters syntactically is that options have values preceded
  // by a colon:
  //   filter1,filter2,filter3,option1:value1,option2:value2

  // Add any relevant enabled filters.
  int num_filters;
  const RewriteOptions::Filter* filters = filter->RelatedFilters(&num_filters);
  for (int i = 0; i < num_filters; ++i) {
    RewriteOptions::Filter filter_enum = filters[i];
    if (options->Enabled(filter_enum)) {
      StrAppend(&value, prefix, RewriteOptions::FilterId(filter_enum));
      prefix = kResourceFilterSeparator;
    }
  }

  // Add any non-default options.
  GoogleString option_value;
  const StringPieceVector* opts = filter->RelatedOptions();
  for (int i = 0, n = (opts == NULL ? 0 : opts->size()); i < n; ++i) {
    StringPiece option = (*opts)[i];
    const char* id;
    bool was_set = false;
    if (options->OptionValue(option, &id, &was_set, &option_value) && was_set) {
      StrAppend(&value, prefix, id, kResourceOptionValueSeparator,
                option_value);
      prefix = kResourceFilterSeparator;
    }
  }
  return value;
}

RewriteQuery::Status RewriteQuery::ParseResourceOption(
    StringPiece value, RewriteOptions* options, const RewriteFilter* filter) {
  Status status = kNoneFound;
  StringPieceVector filters_and_options;
  SplitStringPieceToVector(value, kResourceFilterSeparator,
                           &filters_and_options, true);

  // We will want to validate any filters & options we are trying to set
  // with this mechanism against the whitelist of whatever the filter thinks is
  // needed.  But do this lazily.
  int num_filters;
  const RewriteOptions::Filter* filters = filter->RelatedFilters(&num_filters);
  const StringPieceVector* opts = filter->RelatedOptions();

  for (int i = 0, n = filters_and_options.size(); i < n; ++i) {
    StringPieceVector name_value;
    SplitStringPieceToVector(filters_and_options[i],
                             kResourceOptionValueSeparator, &name_value, true);
    switch (name_value.size()) {
      case 1: {
        RewriteOptions::Filter filter_enum =
            RewriteOptions::LookupFilterById(name_value[0]);
        if ((filter_enum == RewriteOptions::kEndOfFilters) ||
            !std::binary_search(filters, filters + num_filters, filter_enum)) {
          status = kInvalid;
        } else {
          options->EnableFilter(filter_enum);
          status = kSuccess;
        }
        break;
      }
      case 2: {
        StringPiece option_name =
            RewriteOptions::LookupOptionNameById(name_value[0]);
        if (!option_name.empty() &&
            opts != NULL &&
            std::binary_search(opts->begin(), opts->end(), option_name) &&
            options->SetOptionFromName(option_name, name_value[1])
            == RewriteOptions::kOptionOk) {
          status = kSuccess;
        } else {
          status = kInvalid;
        }
        break;
      }
      default:
        status = kInvalid;
    }
  }
  options->SetRewriteLevel(RewriteOptions::kPassThrough);
  options->DisableAllFiltersNotExplicitlyEnabled();
  return status;
}

bool RewriteQuery::ParseProxyMode(
    const GoogleString* mode_name, ProxyMode* mode) {
  int mode_value = 0;
  if (mode_name != NULL &&
      !mode_name->empty() &&
      StringToInt(*mode_name, &mode_value) &&
      mode_value >= kProxyModeDefault &&
      mode_value <= kProxyModeNoTransform) {
    *mode = static_cast<ProxyMode>(mode_value);
    return true;
  }
  return false;
}

bool RewriteQuery::ParseImageQualityPreference(
    const GoogleString* preference_value,
    DeviceProperties::ImageQualityPreference* preference) {
  int value = 0;
  if (preference_value != NULL &&
      !preference_value->empty() &&
      StringToInt(*preference_value, &value) &&
      value >= DeviceProperties::kImageQualityDefault &&
      value <= DeviceProperties::kImageQualityHigh) {
    *preference = static_cast<DeviceProperties::ImageQualityPreference>(value);
    return true;
  }
  return false;
}

bool RewriteQuery::ParseClientOptions(
    const StringPiece& client_options, ProxyMode* proxy_mode,
    DeviceProperties::ImageQualityPreference* image_quality_preference) {
  StringMultiMapSensitive options;
  options.AddFromNameValuePairs(
      client_options, kProxyOptionSeparator, kProxyOptionValueSeparator,
      true);

  const GoogleString* version_value = options.Lookup1(kProxyOptionVersion);
  // We only support version value of kProxyOptionValidVersionValue for now.
  // New supported version might be added later.
  if (version_value != NULL &&
      *version_value == kProxyOptionValidVersionValue) {
    *proxy_mode = kProxyModeDefault;
    *image_quality_preference = DeviceProperties::kImageQualityDefault;
    ParseProxyMode(options.Lookup1(kProxyOptionMode), proxy_mode);

    if (*proxy_mode == kProxyModeDefault) {
      ParseImageQualityPreference(
          options.Lookup1(kProxyOptionImageQualityPreference),
          image_quality_preference);
    }
    return true;
  }
  return false;
}

bool RewriteQuery::UpdateRewriteOptionsWithClientOptions(
    StringPiece client_options, RequestProperties* request_properties,
    RewriteOptions* options) {
  ProxyMode proxy_mode = kProxyModeDefault;
  DeviceProperties::ImageQualityPreference quality_preference =
      DeviceProperties::kImageQualityDefault;
  if (!ParseClientOptions(client_options, &proxy_mode, &quality_preference)) {
    return false;
  }

  if (proxy_mode == kProxyModeNoTransform) {
    options->DisableAllFilters();
    return true;
  } else if (proxy_mode == kProxyModeNoImageTransform) {
    ImageRewriteFilter::DisableRelatedFilters(options);
    return true;
  } else if (proxy_mode == kProxyModeDefault) {
    return false;
  }
  DCHECK(false);
  return false;
}

}  // namespace net_instaweb
