blob: bb10b54b86589242dfc5cf6020ec90b2cd661151 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/renderer/manifest/manifest_parser.h"
#include <stddef.h>
#include "base/json/json_reader.h"
#include "base/strings/nullable_string16.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "content/public/common/manifest_util.h"
#include "content/renderer/manifest/manifest_uma_util.h"
#include "net/base/mime_util.h"
#include "third_party/blink/public/platform/web_icon_sizes_parser.h"
#include "third_party/blink/public/platform/web_size.h"
#include "third_party/blink/public/platform/web_string.h"
#include "third_party/blink/public/web/web_css_parser.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/gfx/geometry/size.h"
namespace {
bool IsValidMimeType(const std::string& mime_type) {
if (mime_type.length() > 0 && mime_type.at(0) == '.')
return true;
return net::ParseMimeTypeWithoutParameter(mime_type, nullptr, nullptr);
}
bool VerifyFiles(const std::vector<blink::Manifest::ShareTargetFile>& files) {
for (const blink::Manifest::ShareTargetFile& file : files) {
for (const base::string16& utf_accept : file.accept) {
std::string accept_type =
base::ToLowerASCII(base::UTF16ToASCII(utf_accept));
if (!IsValidMimeType(accept_type))
return false;
}
}
return true;
}
} // anonymous namespace
namespace content {
ManifestParser::ManifestParser(const base::StringPiece& data,
const GURL& manifest_url,
const GURL& document_url)
: data_(data),
manifest_url_(manifest_url),
document_url_(document_url),
failed_(false) {
}
ManifestParser::~ManifestParser() {
}
void ManifestParser::Parse() {
std::string error_msg;
int error_line = 0;
int error_column = 0;
std::unique_ptr<base::Value> value = base::JSONReader::ReadAndReturnError(
data_, base::JSON_PARSE_RFC, nullptr, &error_msg, &error_line,
&error_column);
if (!value) {
AddErrorInfo(error_msg, true, error_line, error_column);
ManifestUmaUtil::ParseFailed();
failed_ = true;
return;
}
base::DictionaryValue* dictionary = nullptr;
if (!value->GetAsDictionary(&dictionary)) {
AddErrorInfo("root element must be a valid JSON object.", true);
ManifestUmaUtil::ParseFailed();
failed_ = true;
return;
}
DCHECK(dictionary);
manifest_.name = ParseName(*dictionary);
manifest_.short_name = ParseShortName(*dictionary);
manifest_.start_url = ParseStartURL(*dictionary);
manifest_.scope = ParseScope(*dictionary, manifest_.start_url);
manifest_.display = ParseDisplay(*dictionary);
manifest_.orientation = ParseOrientation(*dictionary);
manifest_.icons = ParseIcons(*dictionary);
manifest_.share_target = ParseShareTarget(*dictionary);
manifest_.related_applications = ParseRelatedApplications(*dictionary);
manifest_.prefer_related_applications =
ParsePreferRelatedApplications(*dictionary);
manifest_.theme_color = ParseThemeColor(*dictionary);
manifest_.background_color = ParseBackgroundColor(*dictionary);
manifest_.splash_screen_url = ParseSplashScreenURL(*dictionary);
manifest_.gcm_sender_id = ParseGCMSenderID(*dictionary);
ManifestUmaUtil::ParseSucceeded(manifest_);
}
const blink::Manifest& ManifestParser::manifest() const {
return manifest_;
}
void ManifestParser::TakeErrors(
std::vector<blink::mojom::ManifestErrorPtr>* errors) {
errors->clear();
errors->swap(errors_);
}
bool ManifestParser::failed() const {
return failed_;
}
bool ManifestParser::ParseBoolean(const base::DictionaryValue& dictionary,
const std::string& key,
bool default_value) {
if (!dictionary.HasKey(key))
return default_value;
bool value;
if (!dictionary.GetBoolean(key, &value)) {
AddErrorInfo("property '" + key + "' ignored, type " +
"boolean expected.");
return default_value;
}
return value;
}
base::NullableString16 ManifestParser::ParseString(
const base::DictionaryValue& dictionary,
const std::string& key,
TrimType trim) {
if (!dictionary.HasKey(key))
return base::NullableString16();
base::string16 value;
if (!dictionary.GetString(key, &value)) {
AddErrorInfo("property '" + key + "' ignored, type " +
"string expected.");
return base::NullableString16();
}
if (trim == Trim)
base::TrimWhitespace(value, base::TRIM_ALL, &value);
return base::NullableString16(value, false);
}
base::Optional<SkColor> ManifestParser::ParseColor(
const base::DictionaryValue& dictionary,
const std::string& key) {
base::NullableString16 parsed_color = ParseString(dictionary, key, Trim);
if (parsed_color.is_null())
return base::nullopt;
SkColor color;
if (!blink::WebCSSParser::ParseColor(
&color, blink::WebString::FromUTF16(parsed_color.string()))) {
AddErrorInfo("property '" + key + "' ignored, '" +
base::UTF16ToUTF8(parsed_color.string()) + "' is not a " +
"valid color.");
return base::nullopt;
}
return color;
}
GURL ManifestParser::ParseURL(const base::DictionaryValue& dictionary,
const std::string& key,
const GURL& base_url,
ParseURLOriginRestrictions origin_restriction) {
base::NullableString16 url_str = ParseString(dictionary, key, NoTrim);
if (url_str.is_null())
return GURL();
GURL resolved = base_url.Resolve(url_str.string());
if (!resolved.is_valid()) {
AddErrorInfo("property '" + key + "' ignored, URL is invalid.");
return GURL();
}
switch (origin_restriction) {
case ParseURLOriginRestrictions::kSameOriginOnly:
if (resolved.GetOrigin() != document_url_.GetOrigin()) {
AddErrorInfo("property '" + key +
"' ignored, should be same origin as document.");
return GURL();
}
return resolved;
case ParseURLOriginRestrictions::kNoRestrictions:
return resolved;
}
NOTREACHED();
return GURL();
}
base::NullableString16 ManifestParser::ParseName(
const base::DictionaryValue& dictionary) {
return ParseString(dictionary, "name", Trim);
}
base::NullableString16 ManifestParser::ParseShortName(
const base::DictionaryValue& dictionary) {
return ParseString(dictionary, "short_name", Trim);
}
GURL ManifestParser::ParseStartURL(const base::DictionaryValue& dictionary) {
return ParseURL(dictionary, "start_url", manifest_url_,
ParseURLOriginRestrictions::kSameOriginOnly);
}
GURL ManifestParser::ParseScope(const base::DictionaryValue& dictionary,
const GURL& start_url) {
GURL scope = ParseURL(dictionary, "scope", manifest_url_,
ParseURLOriginRestrictions::kSameOriginOnly);
if (!scope.is_empty()) {
// According to the spec, if the start_url cannot be parsed, the document
// URL should be used as the start URL. If the start_url could not be
// parsed, check that the document URL is within scope.
GURL check_in_scope = start_url.is_empty() ? document_url_ : start_url;
if (check_in_scope.GetOrigin() != scope.GetOrigin() ||
!base::StartsWith(check_in_scope.path(), scope.path(),
base::CompareCase::SENSITIVE)) {
AddErrorInfo(
"property 'scope' ignored. Start url should be within scope "
"of scope URL.");
return GURL();
}
}
return scope;
}
blink::WebDisplayMode ManifestParser::ParseDisplay(
const base::DictionaryValue& dictionary) {
base::NullableString16 display = ParseString(dictionary, "display", Trim);
if (display.is_null())
return blink::kWebDisplayModeUndefined;
blink::WebDisplayMode display_enum =
WebDisplayModeFromString(base::UTF16ToUTF8(display.string()));
if (display_enum == blink::kWebDisplayModeUndefined)
AddErrorInfo("unknown 'display' value ignored.");
return display_enum;
}
blink::WebScreenOrientationLockType ManifestParser::ParseOrientation(
const base::DictionaryValue& dictionary) {
base::NullableString16 orientation =
ParseString(dictionary, "orientation", Trim);
if (orientation.is_null())
return blink::kWebScreenOrientationLockDefault;
blink::WebScreenOrientationLockType orientation_enum =
WebScreenOrientationLockTypeFromString(
base::UTF16ToUTF8(orientation.string()));
if (orientation_enum == blink::kWebScreenOrientationLockDefault)
AddErrorInfo("unknown 'orientation' value ignored.");
return orientation_enum;
}
GURL ManifestParser::ParseIconSrc(const base::DictionaryValue& icon) {
return ParseURL(icon, "src", manifest_url_,
ParseURLOriginRestrictions::kNoRestrictions);
}
base::string16 ManifestParser::ParseIconType(
const base::DictionaryValue& icon) {
base::NullableString16 nullable_string = ParseString(icon, "type", Trim);
if (nullable_string.is_null())
return base::string16();
return nullable_string.string();
}
std::vector<gfx::Size> ManifestParser::ParseIconSizes(
const base::DictionaryValue& icon) {
base::NullableString16 sizes_str = ParseString(icon, "sizes", NoTrim);
std::vector<gfx::Size> sizes;
if (sizes_str.is_null())
return sizes;
blink::WebVector<blink::WebSize> web_sizes =
blink::WebIconSizesParser::ParseIconSizes(
blink::WebString::FromUTF16(sizes_str.string()));
sizes.resize(web_sizes.size());
for (size_t i = 0; i < web_sizes.size(); ++i)
sizes[i] = web_sizes[i];
if (sizes.empty()) {
AddErrorInfo("found icon with no valid size.");
}
return sizes;
}
base::Optional<std::vector<blink::Manifest::ImageResource::Purpose>>
ManifestParser::ParseIconPurpose(const base::DictionaryValue& icon) {
base::NullableString16 purpose_str = ParseString(icon, "purpose", NoTrim);
std::vector<blink::Manifest::ImageResource::Purpose> purposes;
if (purpose_str.is_null()) {
purposes.push_back(blink::Manifest::ImageResource::Purpose::ANY);
return purposes;
}
std::vector<base::string16> keywords = base::SplitString(
purpose_str.string(), base::ASCIIToUTF16(" "),
base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
// "any" is the default if there are no other keywords.
if (keywords.empty()) {
purposes.push_back(blink::Manifest::ImageResource::Purpose::ANY);
return purposes;
}
bool unrecognised_purpose = false;
for (const base::string16& keyword : keywords) {
if (base::LowerCaseEqualsASCII(keyword, "any")) {
purposes.push_back(blink::Manifest::ImageResource::Purpose::ANY);
} else if (base::LowerCaseEqualsASCII(keyword, "badge")) {
purposes.push_back(blink::Manifest::ImageResource::Purpose::BADGE);
} else if (base::LowerCaseEqualsASCII(keyword, "maskable")) {
purposes.push_back(blink::Manifest::ImageResource::Purpose::MASKABLE);
} else {
unrecognised_purpose = true;
}
}
// This implies there was at least one purpose given, but none recognised.
// Instead of defaulting to "any" (which would not be future proof),
// invalidate the whole icon.
if (purposes.empty()) {
AddErrorInfo("found icon with no valid purpose; ignoring it.");
return base::nullopt;
} else if (unrecognised_purpose) {
AddErrorInfo(
"found icon with one or more invalid purposes; those purposes are "
"ignored.");
}
return purposes;
}
std::vector<blink::Manifest::ImageResource> ManifestParser::ParseIcons(
const base::DictionaryValue& dictionary) {
std::vector<blink::Manifest::ImageResource> icons;
if (!dictionary.HasKey("icons"))
return icons;
const base::ListValue* icons_list = nullptr;
if (!dictionary.GetList("icons", &icons_list)) {
AddErrorInfo("property 'icons' ignored, type array expected.");
return icons;
}
for (size_t i = 0; i < icons_list->GetSize(); ++i) {
const base::DictionaryValue* icon_dictionary = nullptr;
if (!icons_list->GetDictionary(i, &icon_dictionary))
continue;
blink::Manifest::ImageResource icon;
icon.src = ParseIconSrc(*icon_dictionary);
// An icon MUST have a valid src. If it does not, it MUST be ignored.
if (!icon.src.is_valid())
continue;
icon.type = ParseIconType(*icon_dictionary);
icon.sizes = ParseIconSizes(*icon_dictionary);
auto purpose = ParseIconPurpose(*icon_dictionary);
if (!purpose)
continue;
icon.purpose = std::move(*purpose);
icons.push_back(icon);
}
return icons;
}
base::string16 ManifestParser::ParseShareTargetFileName(
const base::DictionaryValue& file) {
if (!file.HasKey("name")) {
AddErrorInfo("property 'name' missing.");
return base::string16();
}
base::string16 value;
if (!file.GetString("name", &value)) {
AddErrorInfo("property 'name' ignored, type string expected.");
return base::string16();
}
return value;
}
std::vector<base::string16> ManifestParser::ParseShareTargetFileAccept(
const base::DictionaryValue& dictionary) {
std::vector<base::string16> accept_types;
if (!dictionary.HasKey("accept")) {
return accept_types;
}
base::string16 accept_str;
if (dictionary.GetString("accept", &accept_str)) {
accept_types.push_back(accept_str);
return accept_types;
}
const base::ListValue* accept_list = nullptr;
if (!dictionary.GetList("accept", &accept_list)) {
// 'accept' property is the wrong type. Returning an empty vector here
// causes the 'files' entry to be discarded.
AddErrorInfo("property 'accept' ignored, type array or string expected.");
return accept_types;
}
for (const base::Value& accept_value : accept_list->GetList()) {
if (!accept_value.is_string()) {
// A particular 'accept' entry is invalid - just drop that one entry.
AddErrorInfo("'accept' entry ignored, expected to be of type string.");
continue;
}
accept_types.push_back(base::ASCIIToUTF16(accept_value.GetString()));
}
return accept_types;
}
std::vector<blink::Manifest::ShareTargetFile>
ManifestParser::ParseShareTargetFiles(
const base::DictionaryValue& share_target_params) {
std::vector<blink::Manifest::ShareTargetFile> files;
if (!share_target_params.HasKey("files"))
return files;
const base::ListValue* file_list = nullptr;
if (!share_target_params.GetList("files", &file_list)) {
// https://wicg.github.io/web-share-target/level-2/#share_target-member
// step 5 indicates that the 'files' attribute is allowed to be a single
// (non-array) ShareTargetFile.
const base::DictionaryValue* file_dictionary = nullptr;
if (!share_target_params.GetDictionary("files", &file_dictionary)) {
AddErrorInfo(
"property 'files' ignored, type array or ShareTargetFile expected.");
return files;
}
ParseShareTargetFile(*file_dictionary, &files);
return files;
}
for (const base::Value& file_value : file_list->GetList()) {
const base::DictionaryValue* file_dictionary = nullptr;
if (!file_value.GetAsDictionary(&file_dictionary)) {
AddErrorInfo("files must be a sequence of non-empty file entries.");
continue;
}
ParseShareTargetFile(*file_dictionary, &files);
}
return files;
}
void ManifestParser::ParseShareTargetFile(
const base::DictionaryValue& file_dictionary,
std::vector<blink::Manifest::ShareTargetFile>* files) {
blink::Manifest::ShareTargetFile file;
file.name = ParseShareTargetFileName(file_dictionary);
if (file.name.empty()) {
// https://wicg.github.io/web-share-target/level-2/#share_target-member
// step 7.1 requires that we invalidate this ShareTargetFile if 'name' is an
// empty string. We also invalidate if 'name' is undefined or not a
// string.
return;
}
file.accept = ParseShareTargetFileAccept(file_dictionary);
if (file.accept.empty())
return;
files->push_back(file);
}
base::Optional<blink::Manifest::ShareTarget::Method>
ManifestParser::ParseShareTargetMethod(
const base::DictionaryValue& share_target_dict) {
if (!share_target_dict.HasKey("method")) {
AddErrorInfo(
"Method should be set to either GET or POST. It currently defaults to "
"GET.");
return base::Optional<blink::Manifest::ShareTarget::Method>(
blink::Manifest::ShareTarget::Method::kGet);
}
base::string16 value;
if (!share_target_dict.GetString("method", &value))
return base::nullopt;
std::string method = base::ToUpperASCII(base::UTF16ToASCII(value));
if (method == "GET")
return blink::Manifest::ShareTarget::Method::kGet;
if (method == "POST")
return blink::Manifest::ShareTarget::Method::kPost;
return base::nullopt;
}
base::Optional<blink::Manifest::ShareTarget::Enctype>
ManifestParser::ParseShareTargetEnctype(
const base::DictionaryValue& share_target_dict) {
if (!share_target_dict.HasKey("enctype")) {
AddErrorInfo(
"Enctype should be set to either application/x-www-form-urlencoded or "
"multipart/form-data. It currently defaults to "
"application/x-www-form-urlencoded");
return base::Optional<blink::Manifest::ShareTarget::Enctype>(
blink::Manifest::ShareTarget::Enctype::kApplication);
}
base::string16 value;
if (!share_target_dict.GetString("enctype", &value)) {
return base::nullopt;
}
std::string enctype = base::ToLowerASCII(base::UTF16ToASCII(value));
if (enctype == "application/x-www-form-urlencoded")
return base::Optional<blink::Manifest::ShareTarget::Enctype>(
blink::Manifest::ShareTarget::Enctype::kApplication);
if (enctype == "multipart/form-data")
return base::Optional<blink::Manifest::ShareTarget::Enctype>(
blink::Manifest::ShareTarget::Enctype::kMultipart);
return base::nullopt;
}
blink::Manifest::ShareTargetParams ManifestParser::ParseShareTargetParams(
const base::DictionaryValue& share_target_params) {
blink::Manifest::ShareTargetParams params;
// NOTE: These are key names for query parameters, which are filled with share
// data. As such, |params.url| is just a string.
params.text = ParseString(share_target_params, "text", Trim);
params.title = ParseString(share_target_params, "title", Trim);
params.url = ParseString(share_target_params, "url", Trim);
params.files = ParseShareTargetFiles(share_target_params);
return params;
}
base::Optional<blink::Manifest::ShareTarget> ManifestParser::ParseShareTarget(
const base::DictionaryValue& dictionary) {
if (!dictionary.HasKey("share_target"))
return base::nullopt;
blink::Manifest::ShareTarget share_target;
const base::DictionaryValue* share_target_dict = nullptr;
dictionary.GetDictionary("share_target", &share_target_dict);
share_target.action = ParseURL(*share_target_dict, "action", manifest_url_,
ParseURLOriginRestrictions::kSameOriginOnly);
if (!share_target.action.is_valid()) {
AddErrorInfo(
"property 'share_target' ignored. Property 'action' is "
"invalid.");
return base::nullopt;
}
base::Optional<blink::Manifest::ShareTarget::Method> method =
ParseShareTargetMethod(*share_target_dict);
base::Optional<blink::Manifest::ShareTarget::Enctype> enctype =
ParseShareTargetEnctype(*share_target_dict);
const base::DictionaryValue* share_target_params_dict = nullptr;
if (!share_target_dict->GetDictionary("params", &share_target_params_dict)) {
AddErrorInfo(
"property 'share_target' ignored. Property 'params' type "
"dictionary expected.");
return base::nullopt;
}
share_target.params = ParseShareTargetParams(*share_target_params_dict);
if (method == base::nullopt) {
AddErrorInfo(
"invalid method. Allowed methods are:"
"GET and POST.");
return base::nullopt;
}
if (enctype == base::nullopt) {
AddErrorInfo(
"invalid enctype. Allowed enctypes are:"
"application/x-www-form-urlencoded and multipart/form-data.");
return base::nullopt;
}
if (method == base::Optional<blink::Manifest::ShareTarget::Method>(
blink::Manifest::ShareTarget::Method::kGet)) {
share_target.method = blink::Manifest::ShareTarget::Method::kGet;
} else {
share_target.method = blink::Manifest::ShareTarget::Method::kPost;
}
if (enctype == base::Optional<blink::Manifest::ShareTarget::Enctype>(
blink::Manifest::ShareTarget::Enctype::kMultipart)) {
share_target.enctype = blink::Manifest::ShareTarget::Enctype::kMultipart;
} else {
share_target.enctype = blink::Manifest::ShareTarget::Enctype::kApplication;
}
if (share_target.method == blink::Manifest::ShareTarget::Method::kGet) {
if (share_target.enctype ==
blink::Manifest::ShareTarget::Enctype::kMultipart) {
AddErrorInfo(
"invalid enctype for GET method. Only "
"application/x-www-form-urlencoded is allowed.");
return base::nullopt;
}
}
if (share_target.params.files.size() > 0) {
if (share_target.method != blink::Manifest::ShareTarget::Method::kPost ||
share_target.enctype !=
blink::Manifest::ShareTarget::Enctype::kMultipart) {
AddErrorInfo("files are only supported with multipart/form-data POST.");
return base::nullopt;
}
}
if (!VerifyFiles(share_target.params.files)) {
AddErrorInfo("invalid mime type inside files.");
return base::nullopt;
}
return base::Optional<blink::Manifest::ShareTarget>(share_target);
}
base::NullableString16 ManifestParser::ParseRelatedApplicationPlatform(
const base::DictionaryValue& application) {
return ParseString(application, "platform", Trim);
}
GURL ManifestParser::ParseRelatedApplicationURL(
const base::DictionaryValue& application) {
return ParseURL(application, "url", manifest_url_,
ParseURLOriginRestrictions::kNoRestrictions);
}
base::NullableString16 ManifestParser::ParseRelatedApplicationId(
const base::DictionaryValue& application) {
return ParseString(application, "id", Trim);
}
std::vector<blink::Manifest::RelatedApplication>
ManifestParser::ParseRelatedApplications(
const base::DictionaryValue& dictionary) {
std::vector<blink::Manifest::RelatedApplication> applications;
if (!dictionary.HasKey("related_applications"))
return applications;
const base::ListValue* applications_list = nullptr;
if (!dictionary.GetList("related_applications", &applications_list)) {
AddErrorInfo("property 'related_applications' ignored,"
" type array expected.");
return applications;
}
for (size_t i = 0; i < applications_list->GetSize(); ++i) {
const base::DictionaryValue* application_dictionary = nullptr;
if (!applications_list->GetDictionary(i, &application_dictionary))
continue;
blink::Manifest::RelatedApplication application;
application.platform =
ParseRelatedApplicationPlatform(*application_dictionary);
// "If platform is undefined, move onto the next item if any are left."
if (application.platform.is_null()) {
AddErrorInfo("'platform' is a required field, related application"
" ignored.");
continue;
}
application.id = ParseRelatedApplicationId(*application_dictionary);
application.url = ParseRelatedApplicationURL(*application_dictionary);
// "If both id and url are undefined, move onto the next item if any are
// left."
if (application.url.is_empty() && application.id.is_null()) {
AddErrorInfo("one of 'url' or 'id' is required, related application"
" ignored.");
continue;
}
applications.push_back(application);
}
return applications;
}
bool ManifestParser::ParsePreferRelatedApplications(
const base::DictionaryValue& dictionary) {
return ParseBoolean(dictionary, "prefer_related_applications", false);
}
base::Optional<SkColor> ManifestParser::ParseThemeColor(
const base::DictionaryValue& dictionary) {
return ParseColor(dictionary, "theme_color");
}
base::Optional<SkColor> ManifestParser::ParseBackgroundColor(
const base::DictionaryValue& dictionary) {
return ParseColor(dictionary, "background_color");
}
GURL ManifestParser::ParseSplashScreenURL(
const base::DictionaryValue& dictionary) {
return ParseURL(dictionary, "splash_screen_url", manifest_url_,
ParseURLOriginRestrictions::kSameOriginOnly);
}
base::NullableString16 ManifestParser::ParseGCMSenderID(
const base::DictionaryValue& dictionary) {
return ParseString(dictionary, "gcm_sender_id", Trim);
}
void ManifestParser::AddErrorInfo(const std::string& error_msg,
bool critical,
int error_line,
int error_column) {
errors_.push_back(
{base::in_place, error_msg, critical, error_line, error_column});
}
} // namespace content