blob: 8691df80ea262065708a09debff38597da981ac9 [file] [log] [blame]
// Copyright 2011 Google Inc.
//
// 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.
#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 insert js. Otherwise, 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.
//
// 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.
if (request_headers != NULL && request_headers->IsXmlHttpRequest()) {
if (options_.get() == NULL) {
options_.reset(factory->NewRewriteOptionsForQuery());
}
options_->DisableFiltersRequiringScriptExecution();
options_->DisableFilter(RewriteOptions::kPrioritizeCriticalCss);
}
// 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), NULL);
}
}
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://code.google.com/p/modpagespeed/issues/detail?id=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::kEnabledOff);
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