blob: 3c2ded06d8956fd1ea49da6f70a079c0599b9d35 [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/manifest/manifest_parser.h"
#include <cstddef>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/compiler_specific.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "net/base/mime_util.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "services/device/public/mojom/screen_orientation_lock_types.mojom-blink.h"
#include "services/network/public/cpp/permissions_policy/origin_with_possible_wildcards.h"
#include "services/network/public/cpp/permissions_policy/permissions_policy_declaration.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/manifest/manifest_util.h"
#include "third_party/blink/public/common/safe_url_pattern.h"
#include "third_party/blink/public/common/security/protocol_handler_security_level.h"
#include "third_party/blink/public/mojom/manifest/display_mode.mojom-blink.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom-blink.h"
#include "third_party/blink/public/mojom/manifest/manifest_launch_handler.mojom-blink.h"
#include "third_party/blink/public/platform/url_conversion.h"
#include "third_party/blink/public/platform/web_icon_sizes_parser.h"
#include "third_party/blink/public/platform/web_string.h"
#include "third_party/blink/renderer/core/css/media_list.h"
#include "third_party/blink/renderer/core/css/parser/css_parser.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
#include "third_party/blink/renderer/core/permissions_policy/permissions_policy_parser.h"
#include "third_party/blink/renderer/core/permissions_policy/policy_helper.h"
#include "third_party/blink/renderer/modules/navigatorcontentutils/navigator_content_utils.h"
#include "third_party/blink/renderer/platform/graphics/color.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/json/json_parser.h"
#include "third_party/blink/renderer/platform/json/json_values.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/hash_map.h"
#include "third_party/blink/renderer/platform/wtf/text/strcat.h"
#include "third_party/blink/renderer/platform/wtf/text/string_impl.h"
#include "third_party/blink/renderer/platform/wtf/text/string_utf8_adaptor.h"
#include "third_party/blink/renderer/platform/wtf/text/string_view.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_uchar.h"
#include "third_party/blink/renderer/platform/wtf/wtf_size_t.h"
#include "third_party/icu/source/common/unicode/locid.h"
#include "third_party/liburlpattern/parse.h"
#include "third_party/liburlpattern/part.h"
#include "third_party/liburlpattern/pattern.h"
#include "third_party/liburlpattern/utils.h"
#include "url/url_constants.h"
#include "url/url_util.h"
namespace blink {
namespace {
static constexpr char kScopeExtensionsMissingKeysErrorMessage[] =
"scope_extensions entry ignored, required properties 'type' and 'origin' "
"are missing.";
static constexpr char kScopeExtensionsTypeKey[] = "type";
static constexpr char kScopeExtensionsOriginKey[] = "origin";
static constexpr char kOriginWildcardPrefix[] = "%2A.";
// Keep in sync with web_app_origin_association_task.cc.
static wtf_size_t kMaxScopeExtensionsSize = 10;
static wtf_size_t kMaxShortcutsSize = 10;
static wtf_size_t kMaxOriginLength = 2000;
// The max number of file extensions an app can handle via the File Handling
// API.
const int kFileHandlerExtensionLimit = 300;
int g_file_handler_extension_limit_for_testing = 0;
bool IsValidMimeType(const String& mime_type) {
if (mime_type.StartsWith('.')) {
return true;
}
return net::ParseMimeTypeWithoutParameter(mime_type.Utf8(), nullptr, nullptr);
}
bool VerifyFiles(const Vector<mojom::blink::ManifestFileFilterPtr>& files) {
for (const auto& file : files) {
for (const auto& accept_type : file->accept) {
if (!IsValidMimeType(accept_type.LowerASCII())) {
return false;
}
}
}
return true;
}
// Determines whether |url| is within scope of |scope|.
bool URLIsWithinScope(const KURL& url, const KURL& scope) {
return SecurityOrigin::AreSameOrigin(url, scope) &&
url.GetPath().ToString().StartsWith(scope.GetPath());
}
bool IsHostValidForScopeExtension(String host) {
if (url::HostIsIPAddress(host.Utf8())) {
return true;
}
const size_t registry_length =
net::registry_controlled_domains::PermissiveGetHostRegistryLength(
host.Utf8(),
// Reject unknown registries (registries that don't have any matches
// in effective TLD names).
net::registry_controlled_domains::EXCLUDE_UNKNOWN_REGISTRIES,
// Skip matching private registries that allow external users to
// specify sub-domains, e.g. glitch.me, as this is allowed.
net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES);
// Host cannot be a TLD or invalid.
if (registry_length == 0 || registry_length == std::string::npos ||
registry_length >= host.length()) {
return false;
}
return true;
}
static bool IsCrLfOrTabChar(UChar c) {
return c == '\n' || c == '\r' || c == '\t';
}
std::optional<mojom::blink::ManifestFileHandler::LaunchType>
FileHandlerLaunchTypeFromString(const std::string& launch_type) {
if (EqualIgnoringASCIICase(String(launch_type), "single-client")) {
return mojom::blink::ManifestFileHandler::LaunchType::kSingleClient;
}
if (EqualIgnoringASCIICase(String(launch_type), "multiple-clients")) {
return mojom::blink::ManifestFileHandler::LaunchType::kMultipleClients;
}
return std::nullopt;
}
bool IsDefaultManifest(const mojom::blink::Manifest& manifest,
const KURL& document_url) {
if (manifest.has_custom_id || manifest.has_valid_specified_start_url) {
return false;
}
auto default_manifest = mojom::blink::Manifest::New();
default_manifest->start_url = document_url;
KURL default_id = document_url;
default_id.RemoveFragmentIdentifier();
default_manifest->id = default_id;
default_manifest->scope = KURL(document_url.BaseAsString().ToString());
return manifest == *default_manifest;
}
static const char kUMAIdParseResult[] = "Manifest.ParseIdResult";
// Record that the Manifest was successfully parsed. If it is a default
// Manifest, it will recorded as so and nothing will happen. Otherwise, the
// presence of each properties will be recorded.
void ParseSucceeded(const mojom::blink::ManifestPtr& manifest,
const KURL& document_url) {
if (IsDefaultManifest(*manifest, document_url)) {
return;
}
base::UmaHistogramBoolean("Manifest.HasProperty.name",
!manifest->name.empty());
base::UmaHistogramBoolean("Manifest.HasProperty.short_name",
!manifest->short_name.empty());
base::UmaHistogramBoolean("Manifest.HasProperty.description",
!manifest->description.empty());
base::UmaHistogramBoolean("Manifest.HasProperty.start_url",
!manifest->start_url.IsEmpty());
base::UmaHistogramBoolean(
"Manifest.HasProperty.display",
manifest->display != blink::mojom::DisplayMode::kUndefined);
base::UmaHistogramBoolean(
"Manifest.HasProperty.orientation",
manifest->orientation !=
device::mojom::blink::ScreenOrientationLockType::DEFAULT);
base::UmaHistogramBoolean("Manifest.HasProperty.icons",
!manifest->icons.empty());
base::UmaHistogramBoolean("Manifest.HasProperty.screenshots",
!manifest->screenshots.empty());
base::UmaHistogramBoolean("Manifest.HasProperty.share_target",
manifest->share_target.get());
base::UmaHistogramBoolean("Manifest.HasProperty.protocol_handlers",
!manifest->protocol_handlers.empty());
base::UmaHistogramBoolean("Manifest.HasProperty.gcm_sender_id",
!manifest->gcm_sender_id.empty());
}
// Returns a liburlpattern::Part list obtained from running
// liburlpattern::Parse on a UrlPatternInit field. The list will be empty if the
// field is empty. Returns std::nullopt if the field should be rejected or the
// parse failed, e.g. if it contains custom (or ill-formed) regex.
std::optional<std::vector<liburlpattern::Part>> ParsePatternInitField(
const std::optional<String>& field,
const String default_field_value) {
const String value = field.has_value() ? field.value() : default_field_value;
if (value.empty()) {
return std::vector<liburlpattern::Part>();
}
StringUtf8Adaptor utf8(value);
auto parse_result = liburlpattern::Parse(
utf8.AsStringView(),
[](std::string_view input) { return std::string(input); });
if (parse_result.has_value()) {
std::vector<liburlpattern::Part> part_list;
for (auto& part : parse_result.value().PartList()) {
// We don't allow custom regex for security reasons as this will be
// used in the browser process.
if (part.type == liburlpattern::PartType::kRegex) {
return std::nullopt;
}
part_list.push_back(std::move(part));
}
return part_list;
}
return std::nullopt;
}
String EscapePatternString(const StringView& input) {
std::string result;
result.reserve(input.length());
StringUtf8Adaptor utf8(input);
liburlpattern::EscapePatternStringAndAppend(utf8.AsStringView(), result);
return String(result);
}
// Utility function to determine if a pathname is absolute or not. We do some
// additional checking for escaped or grouped slashes.
//
// Note: This is partially copied from
// third_party/blink/renderer/core/url_pattern/url_pattern.cc
bool IsAbsolutePathname(String pathname) {
if (pathname.empty()) {
return false;
}
if (pathname[0] == '/') {
return true;
}
if (pathname.length() < 2) {
return false;
}
// Patterns treat escaped slashes and slashes within an explicit grouping as
// valid leading slashes. For example, "\/foo" or "{/foo}". Patterns do
// not consider slashes within a custom regexp group as valid for the leading
// pathname slash for now. To support that we would need to be able to
// detect things like ":name_123(/foo)" as a valid leading group in a pattern,
// but that is considered too complex for now.
if ((pathname[0] == '\\' || pathname[0] == '{') && pathname[1] == '/') {
return true;
}
return false;
}
String ResolveRelativePathnamePattern(const KURL& base_url, String pathname) {
if (base_url.IsStandard() && !IsAbsolutePathname(pathname)) {
String base_path = EscapePatternString(base_url.GetPath());
auto slash_index = base_path.ReverseFind('/');
if (slash_index != kNotFound) {
// Extract the base_url path up to and including the last slash. Append
// the relative pathname to it.
base_path.Truncate(slash_index + 1);
base_path = StrCat({base_path, pathname});
return base_path;
}
}
return pathname;
}
} // anonymous namespace
ManifestParser::ManifestParser(const String& data,
const KURL& manifest_url,
const KURL& document_url,
ExecutionContext* execution_context)
: data_(data),
manifest_url_(manifest_url),
document_url_(document_url),
execution_context_(execution_context),
failed_(false) {}
ManifestParser::~ManifestParser() = default;
// static
void ManifestParser::SetFileHandlerExtensionLimitForTesting(int limit) {
g_file_handler_extension_limit_for_testing = limit;
}
bool ManifestParser::Parse() {
DCHECK(!manifest_);
// TODO(crbug.com/1264024): Deprecate JSON comments here, if possible.
JSONParseError error;
bool has_comments = false;
std::unique_ptr<JSONValue> root =
ParseJSONWithCommentsDeprecated(data_, &error, &has_comments);
manifest_ = mojom::blink::Manifest::New();
if (!root) {
AddErrorInfo(error.message, true, error.line, error.column);
failed_ = true;
return false;
}
std::unique_ptr<JSONObject> root_object = JSONObject::From(std::move(root));
if (!root_object) {
AddErrorInfo("root element must be a valid JSON object.", true);
failed_ = true;
return false;
}
manifest_->manifest_url = manifest_url_;
manifest_->dir = ParseDir(root_object.get());
manifest_->name = ParseName(root_object.get());
manifest_->short_name = ParseShortName(root_object.get());
manifest_->description = ParseDescription(root_object.get());
const auto& [start_url, start_url_parse_result] =
ParseStartURL(root_object.get(), document_url_);
manifest_->start_url = start_url;
manifest_->has_valid_specified_start_url =
start_url_parse_result == ParseStartUrlResult::kParsedFromJson;
const auto& [id, id_parse_result] =
ParseId(root_object.get(), manifest_->start_url);
manifest_->id = id;
manifest_->has_custom_id = id_parse_result == ParseIdResultType::kSucceed;
manifest_->scope = ParseScope(root_object.get(), manifest_->start_url);
manifest_->display = ParseDisplay(root_object.get());
if (manifest_->display != mojom::blink::DisplayMode::kUndefined) {
UseCounter::Count(execution_context_, WebFeature::kWebAppManifestDisplay);
switch (manifest_->display) {
case blink::mojom::DisplayMode::kBrowser:
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestDisplayBrowser);
break;
case blink::mojom::DisplayMode::kMinimalUi:
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestDisplayMinimalUI);
break;
case blink::mojom::DisplayMode::kFullscreen:
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestDisplayFullscreen);
break;
case blink::mojom::DisplayMode::kStandalone:
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestDisplayStandalone);
break;
default:
break;
}
}
manifest_->display_override = ParseDisplayOverride(root_object.get());
for (const mojom::blink::DisplayMode& display_override :
manifest_->display_override) {
if (display_override == mojom::blink::DisplayMode::kWindowControlsOverlay) {
UseCounter::Count(execution_context_,
WebFeature::kWebAppWindowControlsOverlay);
} else if (display_override == mojom::blink::DisplayMode::kBorderless) {
UseCounter::Count(execution_context_, WebFeature::kWebAppBorderless);
} else if (display_override == mojom::blink::DisplayMode::kTabbed) {
UseCounter::Count(execution_context_, WebFeature::kWebAppTabbed);
}
}
if (base::FeatureList::IsEnabled(blink::features::kWebAppBorderless)) {
manifest_->borderless_url_patterns =
ParseUrlPatterns(root_object.get(), "borderless_url_patterns");
}
manifest_->orientation = ParseOrientation(root_object.get());
manifest_->icons = ParseIcons(root_object.get());
if (!manifest_->icons.empty()) {
UseCounter::Count(execution_context_, WebFeature::kWebAppManifestIcons);
}
manifest_->icons_localized = ParseIconsLocalized(root_object.get());
if (!manifest_->icons_localized.empty()) {
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestIconsLocalized);
}
manifest_->screenshots = ParseScreenshots(root_object.get());
if (!manifest_->screenshots.empty()) {
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestScreenshots);
}
auto share_target = ParseShareTarget(root_object.get());
if (share_target.has_value()) {
manifest_->share_target = std::move(*share_target);
UseCounter::CountWebDXFeature(execution_context_,
WebDXFeature::kAppShareTargets);
}
manifest_->file_handlers = ParseFileHandlers(root_object.get());
manifest_->protocol_handlers = ParseProtocolHandlers(root_object.get());
if (!manifest_->protocol_handlers.empty()) {
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestProtocolHandlers);
}
if (!(execution_context_ && execution_context_->IsIsolatedContext())) {
// TODO(crbug.com/383094092): Scope Extensions for IWAs are not defined yet.
manifest_->scope_extensions = ParseScopeExtensions(root_object.get());
if (!manifest_->scope_extensions.empty()) {
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestScopeExtensions);
}
}
manifest_->lock_screen = ParseLockScreen(root_object.get());
if (!manifest_->lock_screen.is_null()) {
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestLockScreen);
}
manifest_->note_taking = ParseNoteTaking(root_object.get());
if (!manifest_->note_taking.is_null()) {
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestNoteTaking);
}
manifest_->related_applications = ParseRelatedApplications(root_object.get());
if (!manifest_->related_applications.empty()) {
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestRelated_Applications);
}
manifest_->prefer_related_applications =
ParsePreferRelatedApplications(root_object.get());
if (manifest_->prefer_related_applications) {
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestPrefer_Related_Applications);
}
std::optional<RGBA32> theme_color = ParseThemeColor(root_object.get());
manifest_->has_theme_color = theme_color.has_value();
if (manifest_->has_theme_color) {
manifest_->theme_color = *theme_color;
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestThemeColor);
}
std::optional<RGBA32> background_color =
ParseBackgroundColor(root_object.get());
manifest_->has_background_color = background_color.has_value();
if (manifest_->has_background_color) {
manifest_->background_color = *background_color;
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestBackgroundColor);
}
manifest_->gcm_sender_id = ParseGCMSenderID(root_object.get());
manifest_->shortcuts = ParseShortcuts(root_object.get());
if (!manifest_->shortcuts.empty()) {
UseCounter::CountWebDXFeature(execution_context_,
WebDXFeature::kAppShortcuts);
}
manifest_->permissions_policy =
ParseIsolatedAppPermissions(root_object.get());
if (!manifest_->permissions_policy.empty()) {
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestPermissionsPolicy);
}
manifest_->launch_handler = ParseLaunchHandler(root_object.get());
if (!manifest_->launch_handler.is_null()) {
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestLaunchHandler);
}
if (RuntimeEnabledFeatures::WebAppTranslationsEnabled(execution_context_)) {
manifest_->translations = ParseTranslations(root_object.get());
if (!manifest_->translations.empty()) {
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestTranslations);
}
}
if (RuntimeEnabledFeatures::WebAppTabStripCustomizationsEnabled(
execution_context_)) {
manifest_->tab_strip = ParseTabStrip(root_object.get());
if (!manifest_->tab_strip.is_null()) {
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestTabStrip);
}
}
manifest_->version = ParseVersion(root_object.get());
if (!manifest_->version.empty()) {
UseCounter::Count(execution_context_, WebFeature::kWebAppManifestVersion);
}
manifest_->name_localized = ParseNameLocalized(root_object.get());
if (!manifest_->name_localized.empty()) {
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestNameLocalized);
}
manifest_->short_name_localized = ParseShortNameLocalized(root_object.get());
if (!manifest_->short_name_localized.empty()) {
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestShortNameLocalized);
}
manifest_->description_localized =
ParseDescriptionLocalized(root_object.get());
if (!manifest_->description_localized.empty()) {
UseCounter::Count(execution_context_,
WebFeature::kWebAppManifestDescriptionLocalized);
}
ParseSucceeded(manifest_, document_url_);
base::UmaHistogramEnumeration(kUMAIdParseResult, id_parse_result);
return has_comments;
}
mojom::blink::ManifestPtr ManifestParser::TakeManifest() {
return std::move(manifest_);
}
void ManifestParser::TakeErrors(
Vector<mojom::blink::ManifestErrorPtr>* errors) {
errors->clear();
errors->swap(errors_);
}
bool ManifestParser::failed() const {
return failed_;
}
ManifestParser::PatternInit::PatternInit(std::optional<String> protocol,
std::optional<String> username,
std::optional<String> password,
std::optional<String> hostname,
std::optional<String> port,
std::optional<String> pathname,
std::optional<String> search,
std::optional<String> hash,
KURL base_url)
: protocol(std::move(protocol)),
username(std::move(username)),
password(std::move(password)),
hostname(std::move(hostname)),
port(std::move(port)),
pathname(std::move(pathname)),
search(std::move(search)),
hash(std::move(hash)),
base_url(base_url) {}
ManifestParser::PatternInit::~PatternInit() = default;
ManifestParser::PatternInit::PatternInit(PatternInit&&) = default;
ManifestParser::PatternInit& ManifestParser::PatternInit::operator=(
PatternInit&&) = default;
bool ManifestParser::PatternInit::IsAbsolute() const {
return protocol.has_value() || hostname.has_value() || port.has_value();
}
bool ManifestParser::ParseBoolean(const JSONObject* object,
const String& key,
bool default_value) {
JSONValue* json_value = object->Get(key);
if (!json_value) {
return default_value;
}
bool value;
if (!json_value->AsBoolean(&value)) {
AddErrorInfo(
StrCat({"property '", key, "' ignored, type boolean expected."}));
return default_value;
}
return value;
}
std::optional<String> ManifestParser::ParseString(const JSONObject* object,
const String& key,
Trim trim) {
JSONValue* json_value = object->Get(key);
if (!json_value) {
return std::nullopt;
}
String value;
if (!json_value->AsString(&value) || value.IsNull()) {
AddErrorInfo(
StrCat({"property '", key, "' ignored, type string expected."}));
return std::nullopt;
}
if (trim) {
value = value.StripWhiteSpace();
}
return value;
}
std::optional<String> ManifestParser::ParseStringForMember(
const JSONObject* object,
const String& member_name,
const String& key,
bool required,
Trim trim) {
JSONValue* json_value = object->Get(key);
if (!json_value) {
if (required) {
AddErrorInfo(
StrCat({"property '", key, "' of '", member_name, "' not present."}));
}
return std::nullopt;
}
String value;
if (!json_value->AsString(&value)) {
AddErrorInfo(StrCat({"property '", key, "' of '", member_name,
"' ignored, type string expected."}));
return std::nullopt;
}
if (trim) {
value = value.StripWhiteSpace();
}
if (value == "") {
AddErrorInfo(StrCat(
{"property '", key, "' of '", member_name, "' is an empty string."}));
if (required) {
return std::nullopt;
}
}
return value;
}
std::optional<RGBA32> ManifestParser::ParseColor(const JSONObject* object,
const String& key) {
std::optional<String> parsed_color = ParseString(object, key, Trim(true));
if (!parsed_color.has_value()) {
return std::nullopt;
}
Color color;
if (!CSSParser::ParseColor(color, *parsed_color)) {
AddErrorInfo(StrCat({"property '", key, "' ignored, '", *parsed_color,
"' is not a valid color."}));
return std::nullopt;
}
return color.Rgb();
}
KURL ManifestParser::ParseURL(const JSONObject* object,
const String& key,
const KURL& base_url,
ParseURLRestrictions origin_restriction,
bool ignore_empty_string) {
std::optional<String> url_str = ParseString(object, key, Trim(false));
if (!url_str.has_value()) {
return KURL();
}
if (ignore_empty_string && url_str.value() == "") {
return KURL();
}
KURL resolved = KURL(base_url, *url_str);
if (!resolved.IsValid()) {
AddErrorInfo(StrCat({"property '", key, "' ignored, URL is invalid."}));
return KURL();
}
switch (origin_restriction) {
case ParseURLRestrictions::kNoRestrictions:
return resolved;
case ParseURLRestrictions::kSameOriginOnly:
if (!SecurityOrigin::AreSameOrigin(resolved, document_url_)) {
AddErrorInfo(StrCat({"property '", key,
"' ignored, should be same origin as document."}));
return KURL();
}
return resolved;
case ParseURLRestrictions::kWithinScope:
if (!URLIsWithinScope(resolved, manifest_->scope)) {
AddErrorInfo(
StrCat({"property '", key,
"' ignored, should be within scope of the manifest."}));
return KURL();
}
// Within scope implies same origin as document URL.
DCHECK(SecurityOrigin::AreSameOrigin(resolved, document_url_));
return resolved;
}
NOTREACHED();
}
template <typename Enum>
Enum ManifestParser::ParseFirstValidEnum(const JSONObject* object,
const String& key,
Enum (*parse_enum)(const std::string&),
Enum invalid_value) {
const JSONValue* value = object->Get(key);
if (!value) {
return invalid_value;
}
String string_value;
if (value->AsString(&string_value)) {
Enum enum_value = parse_enum(string_value.Utf8());
if (enum_value == invalid_value) {
AddErrorInfo(
StrCat({key, " value '", string_value, "' ignored, unknown value."}));
}
return enum_value;
}
const JSONArray* list = JSONArray::Cast(value);
if (!list) {
AddErrorInfo(
StrCat({"property '", key,
"' ignored, type string or array of strings expected."}));
return invalid_value;
}
for (const JSONValue& entry : *list) {
if (!entry.AsString(&string_value)) {
AddErrorInfo(StrCat({key, " value '", entry.ToJSONString(),
"' ignored, string expected."}));
continue;
}
Enum enum_value = parse_enum(string_value.Utf8());
if (enum_value != invalid_value) {
return enum_value;
}
AddErrorInfo(
StrCat({key, " value '", string_value, "' ignored, unknown value."}));
}
return invalid_value;
}
mojom::blink::Manifest::TextDirection ManifestParser::ParseDir(
const JSONObject* object) {
using TextDirection = mojom::blink::Manifest::TextDirection;
std::optional<String> dir = ParseString(object, "dir", Trim(true));
if (!dir.has_value()) {
return TextDirection::kAuto;
}
std::optional<TextDirection> textDirection =
TextDirectionFromString(dir->Utf8());
if (!textDirection.has_value()) {
AddErrorInfo("unknown 'dir' value ignored.");
return TextDirection::kAuto;
}
return *textDirection;
}
String ManifestParser::ParseName(const JSONObject* object) {
std::optional<String> name = ParseString(object, "name", Trim(true));
if (name.has_value()) {
name = name->RemoveCharacters(IsCrLfOrTabChar);
if (name->length() == 0) {
name = std::nullopt;
}
}
return name.has_value() ? *name : String();
}
String ManifestParser::ParseShortName(const JSONObject* object) {
std::optional<String> short_name =
ParseString(object, "short_name", Trim(true));
if (short_name.has_value()) {
short_name = short_name->RemoveCharacters(IsCrLfOrTabChar);
if (short_name->length() == 0) {
short_name = std::nullopt;
}
}
return short_name.has_value() ? *short_name : String();
}
String ManifestParser::ParseDescription(const JSONObject* object) {
std::optional<String> description =
ParseString(object, "description", Trim(true));
return description.has_value() ? *description : String();
}
std::pair<KURL, ManifestParser::ParseIdResultType> ManifestParser::ParseId(
const JSONObject* object,
const KURL& start_url) {
if (!start_url.IsValid()) {
return {KURL(), ParseIdResultType::kInvalidStartUrl};
}
KURL start_url_origin = KURL(SecurityOrigin::Create(start_url)->ToString());
KURL id = ParseURL(object, "id", start_url_origin,
ParseURLRestrictions::kSameOriginOnly,
/*ignore_empty_string=*/true);
ParseIdResultType parse_result;
if (id.IsValid()) {
parse_result = ParseIdResultType::kSucceed;
UseCounter::Count(execution_context_, WebFeature::kWebAppManifestIdField);
} else {
// If id is not specified, sets to start_url
parse_result = ParseIdResultType::kDefaultToStartUrl;
id = start_url;
}
id.RemoveFragmentIdentifier();
return {id, parse_result};
}
std::pair<KURL, ManifestParser::ParseStartUrlResult>
ManifestParser::ParseStartURL(const JSONObject* object,
const KURL& document_url) {
KURL start_url = ParseURL(object, "start_url", manifest_url_,
ParseURLRestrictions::kSameOriginOnly);
if (start_url.IsEmpty()) {
return std::make_pair(document_url,
ParseStartUrlResult::kDefaultDocumentUrl);
}
UseCounter::Count(execution_context_, WebFeature::kWebAppManifestStartUrl);
return std::make_pair(start_url, ParseStartUrlResult::kParsedFromJson);
}
KURL ManifestParser::ParseScope(const JSONObject* object,
const KURL& start_url) {
KURL scope = ParseURL(object, "scope", manifest_url_,
ParseURLRestrictions::kNoRestrictions);
const KURL& default_value = start_url;
DCHECK(default_value.IsValid());
if (scope.IsEmpty()) {
return KURL(default_value.BaseAsString().ToString());
}
if (!URLIsWithinScope(default_value, scope)) {
AddErrorInfo(
"property 'scope' ignored. Start url should be within scope "
"of scope URL.");
return KURL(default_value.BaseAsString().ToString());
}
scope.RemoveFragmentIdentifier();
scope.SetQuery(String());
DCHECK(scope.IsValid());
DCHECK(SecurityOrigin::AreSameOrigin(scope, document_url_));
UseCounter::Count(execution_context_, WebFeature::kWebAppManifestScope);
return scope;
}
blink::mojom::DisplayMode ManifestParser::ParseDisplay(
const JSONObject* object) {
std::optional<String> display = ParseString(object, "display", Trim(true));
if (!display.has_value()) {
return blink::mojom::DisplayMode::kUndefined;
}
blink::mojom::DisplayMode display_enum =
DisplayModeFromString(display->Utf8());
if (display_enum == mojom::blink::DisplayMode::kUndefined) {
AddErrorInfo("unknown 'display' value ignored.");
return display_enum;
}
// Ignore "enhanced" display modes.
if (!IsBasicDisplayMode(display_enum)) {
display_enum = mojom::blink::DisplayMode::kUndefined;
AddErrorInfo("inapplicable 'display' value ignored.");
}
return display_enum;
}
Vector<mojom::blink::DisplayMode> ManifestParser::ParseDisplayOverride(
const JSONObject* object) {
Vector<mojom::blink::DisplayMode> display_override;
JSONValue* json_value = object->Get("display_override");
if (!json_value) {
return display_override;
}
JSONArray* display_override_list = object->GetArray("display_override");
if (!display_override_list) {
AddErrorInfo("property 'display_override' ignored, type array expected.");
return display_override;
}
for (const JSONValue& value : *display_override_list) {
String display_enum_string;
// AsString will return an empty string if a type error occurs,
// which will cause DisplayModeFromString to return kUndefined,
// resulting in this entry being ignored.
value.AsString(&display_enum_string);
display_enum_string = display_enum_string.StripWhiteSpace();
mojom::blink::DisplayMode display_enum =
DisplayModeFromString(display_enum_string.Utf8());
if (!RuntimeEnabledFeatures::WebAppTabStripEnabled(execution_context_) &&
display_enum == mojom::blink::DisplayMode::kTabbed) {
display_enum = mojom::blink::DisplayMode::kUndefined;
}
if (!base::FeatureList::IsEnabled(blink::features::kWebAppBorderless) &&
display_enum == mojom::blink::DisplayMode::kBorderless) {
display_enum = mojom::blink::DisplayMode::kUndefined;
}
if (display_enum != mojom::blink::DisplayMode::kUndefined) {
display_override.push_back(display_enum);
}
}
return display_override;
}
device::mojom::blink::ScreenOrientationLockType
ManifestParser::ParseOrientation(const JSONObject* object) {
std::optional<String> orientation =
ParseString(object, "orientation", Trim(true));
if (!orientation.has_value()) {
return device::mojom::blink::ScreenOrientationLockType::DEFAULT;
}
device::mojom::blink::ScreenOrientationLockType orientation_enum =
WebScreenOrientationLockTypeFromString(orientation->Utf8());
if (orientation_enum ==
device::mojom::blink::ScreenOrientationLockType::DEFAULT) {
AddErrorInfo("unknown 'orientation' value ignored.");
}
return orientation_enum;
}
KURL ManifestParser::ParseIconSrc(const JSONObject* icon) {
return ParseURL(icon, "src", manifest_url_,
ParseURLRestrictions::kNoRestrictions);
}
String ManifestParser::ParseIconType(const JSONObject* icon) {
std::optional<String> type = ParseString(icon, "type", Trim(true));
return type.value_or(g_empty_string);
}
Vector<gfx::Size> ManifestParser::ParseIconSizes(const JSONObject* icon) {
std::optional<String> sizes_str = ParseString(icon, "sizes", Trim(false));
if (!sizes_str.has_value()) {
return Vector<gfx::Size>();
}
std::vector<gfx::Size> web_sizes =
WebIconSizesParser::ParseIconSizes(WebString(*sizes_str));
Vector<gfx::Size> sizes;
for (auto& size : web_sizes) {
sizes.push_back(size);
}
if (sizes.empty()) {
AddErrorInfo("found icon with no valid size.");
}
return sizes;
}
std::optional<Vector<mojom::blink::ManifestImageResource::Purpose>>
ManifestParser::ParseIconPurpose(const JSONObject* icon) {
std::optional<String> purpose_str = ParseString(icon, "purpose", Trim(false));
Vector<mojom::blink::ManifestImageResource::Purpose> purposes;
if (!purpose_str.has_value()) {
purposes.push_back(mojom::blink::ManifestImageResource::Purpose::ANY);
return purposes;
}
Vector<String> keywords;
purpose_str.value().Split(/*separator=*/" ", /*allow_empty_entries=*/false,
keywords);
// "any" is the default if there are no other keywords.
if (keywords.empty()) {
purposes.push_back(mojom::blink::ManifestImageResource::Purpose::ANY);
return purposes;
}
bool unrecognised_purpose = false;
for (auto& keyword : keywords) {
keyword = keyword.StripWhiteSpace();
if (keyword.empty()) {
continue;
}
if (EqualIgnoringASCIICase(keyword, "any")) {
purposes.push_back(mojom::blink::ManifestImageResource::Purpose::ANY);
} else if (EqualIgnoringASCIICase(keyword, "monochrome")) {
purposes.push_back(
mojom::blink::ManifestImageResource::Purpose::MONOCHROME);
} else if (EqualIgnoringASCIICase(keyword, "maskable")) {
purposes.push_back(
mojom::blink::ManifestImageResource::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 std::nullopt;
}
if (unrecognised_purpose) {
AddErrorInfo(
"found icon with one or more invalid purposes; those purposes are "
"ignored.");
}
return purposes;
}
mojom::blink::ManifestScreenshot::FormFactor
ManifestParser::ParseScreenshotFormFactor(const JSONObject* screenshot) {
std::optional<String> form_factor_str =
ParseString(screenshot, "form_factor", Trim(false));
if (!form_factor_str.has_value()) {
return mojom::blink::ManifestScreenshot::FormFactor::kUnknown;
}
String form_factor = form_factor_str.value();
if (EqualIgnoringASCIICase(form_factor, "wide")) {
return mojom::blink::ManifestScreenshot::FormFactor::kWide;
} else if (EqualIgnoringASCIICase(form_factor, "narrow")) {
return mojom::blink::ManifestScreenshot::FormFactor::kNarrow;
}
AddErrorInfo(
"property 'form_factor' on screenshots has an invalid value, ignoring "
"it.");
return mojom::blink::ManifestScreenshot::FormFactor::kUnknown;
}
String ManifestParser::ParseScreenshotLabel(const JSONObject* object) {
std::optional<String> label = ParseString(object, "label", Trim(true));
return label.has_value() ? *label : String();
}
Vector<mojom::blink::ManifestImageResourcePtr> ManifestParser::ParseIcons(
const JSONObject* object) {
return ParseImageResourceArray("icons", object);
}
HashMap<String, Vector<mojom::blink::ManifestImageResourcePtr>>
ManifestParser::ParseIconsLocalized(const JSONObject* object) {
HashMap<String, Vector<mojom::blink::ManifestImageResourcePtr>>
localized_icons;
JSONValue* json_value = object->Get("icons_localized");
if (!json_value) {
return localized_icons;
}
const JSONObject* icons_localized_object = JSONObject::Cast(json_value);
if (!icons_localized_object) {
AddErrorInfo("property 'icons_localized' ignored, type object expected.");
return localized_icons;
}
for (wtf_size_t i = 0; i < icons_localized_object->size(); ++i) {
const JSONObject::Entry& entry = icons_localized_object->at(i);
const String& locale = entry.first;
Vector<mojom::blink::ManifestImageResourcePtr> icons =
ParseImageResourceArray(locale, icons_localized_object);
if (!icons.empty()) {
localized_icons.Set(locale, std::move(icons));
}
}
return localized_icons;
}
Vector<mojom::blink::ManifestScreenshotPtr> ManifestParser::ParseScreenshots(
const JSONObject* object) {
Vector<mojom::blink::ManifestScreenshotPtr> screenshots;
JSONValue* json_value = object->Get("screenshots");
if (!json_value) {
return screenshots;
}
JSONArray* screenshots_list = object->GetArray("screenshots");
if (!screenshots_list) {
AddErrorInfo("property 'screenshots' ignored, type array expected.");
return screenshots;
}
for (const JSONValue& value : *screenshots_list) {
auto* screenshot_object = JSONObject::Cast(&value);
if (!screenshot_object) {
continue;
}
auto screenshot = mojom::blink::ManifestScreenshot::New();
auto image = ParseImageResource(screenshot_object);
if (!image.has_value()) {
continue;
}
screenshot->image = std::move(*image);
screenshot->form_factor = ParseScreenshotFormFactor(screenshot_object);
screenshot->label = ParseScreenshotLabel(screenshot_object);
screenshots.push_back(std::move(screenshot));
}
return screenshots;
}
Vector<mojom::blink::ManifestImageResourcePtr>
ManifestParser::ParseImageResourceArray(const String& key,
const JSONObject* object) {
Vector<mojom::blink::ManifestImageResourcePtr> icons;
JSONValue* json_value = object->Get(key);
if (!json_value) {
return icons;
}
JSONArray* icons_list = object->GetArray(key);
if (!icons_list) {
AddErrorInfo(
StrCat({"property '", key, "' ignored, type array expected."}));
return icons;
}
for (const JSONValue& value : *icons_list) {
auto icon = ParseImageResource(&value);
if (icon.has_value()) {
icons.push_back(std::move(*icon));
}
}
return icons;
}
std::optional<mojom::blink::ManifestImageResourcePtr>
ManifestParser::ParseImageResource(const JSONValue* object) {
const JSONObject* icon_object = JSONObject::Cast(object);
if (!icon_object) {
return std::nullopt;
}
auto icon = mojom::blink::ManifestImageResource::New();
icon->src = ParseIconSrc(icon_object);
// An icon MUST have a valid src. If it does not, it MUST be ignored.
if (!icon->src.IsValid()) {
return std::nullopt;
}
icon->type = ParseIconType(icon_object);
icon->sizes = ParseIconSizes(icon_object);
auto purpose = ParseIconPurpose(icon_object);
if (!purpose) {
return std::nullopt;
}
icon->purpose = std::move(*purpose);
return icon;
}
String ManifestParser::ParseShortcutName(const JSONObject* shortcut) {
std::optional<String> name =
ParseStringForMember(shortcut, "shortcut", "name", true, Trim(true));
return name.has_value() ? *name : String();
}
String ManifestParser::ParseShortcutShortName(const JSONObject* shortcut) {
std::optional<String> short_name = ParseStringForMember(
shortcut, "shortcut", "short_name", false, Trim(true));
return short_name.has_value() ? *short_name : String();
}
String ManifestParser::ParseShortcutDescription(const JSONObject* shortcut) {
std::optional<String> description = ParseStringForMember(
shortcut, "shortcut", "description", false, Trim(true));
return description.has_value() ? *description : String();
}
KURL ManifestParser::ParseShortcutUrl(const JSONObject* shortcut) {
KURL shortcut_url = ParseURL(shortcut, "url", manifest_url_,
ParseURLRestrictions::kWithinScope);
if (shortcut_url.IsNull()) {
AddErrorInfo("property 'url' of 'shortcut' not present.");
}
return shortcut_url;
}
Vector<mojom::blink::ManifestShortcutItemPtr> ManifestParser::ParseShortcuts(
const JSONObject* object) {
Vector<mojom::blink::ManifestShortcutItemPtr> shortcuts;
JSONValue* json_value = object->Get("shortcuts");
if (!json_value) {
return shortcuts;
}
JSONArray* shortcuts_list = object->GetArray("shortcuts");
if (!shortcuts_list) {
AddErrorInfo("property 'shortcuts' ignored, type array expected.");
return shortcuts;
}
for (wtf_size_t i = 0; i < shortcuts_list->size(); ++i) {
if (i == kMaxShortcutsSize) {
AddErrorInfo(StrCat({"property 'shortcuts' contains more than ",
String::Number(kMaxShortcutsSize),
" valid elements, only the first ",
String::Number(kMaxShortcutsSize), " are parsed."}));
break;
}
JSONObject* shortcut_object = JSONObject::Cast(shortcuts_list->at(i));
if (!shortcut_object) {
continue;
}
auto shortcut = mojom::blink::ManifestShortcutItem::New();
shortcut->url = ParseShortcutUrl(shortcut_object);
// A shortcut MUST have a valid url. If it does not, it MUST be ignored.
if (!shortcut->url.IsValid()) {
continue;
}
// A shortcut MUST have a valid name. If it does not, it MUST be ignored.
shortcut->name = ParseShortcutName(shortcut_object);
if (shortcut->name == String()) {
continue;
}
shortcut->short_name = ParseShortcutShortName(shortcut_object);
shortcut->description = ParseShortcutDescription(shortcut_object);
// Parse localized text fields
shortcut->name_localized = ParseNameLocalized(shortcut_object);
shortcut->short_name_localized = ParseShortNameLocalized(shortcut_object);
shortcut->description_localized =
ParseDescriptionLocalized(shortcut_object);
auto icons = ParseIcons(shortcut_object);
if (!icons.empty()) {
shortcut->icons = std::move(icons);
}
shortcut->icons_localized = ParseIconsLocalized(shortcut_object);
shortcuts.push_back(std::move(shortcut));
}
return shortcuts;
}
String ManifestParser::ParseFileFilterName(const JSONObject* file) {
if (!file->Get("name")) {
AddErrorInfo("property 'name' missing.");
return g_empty_string;
}
String value;
if (!file->GetString("name", &value)) {
AddErrorInfo("property 'name' ignored, type string expected.");
return g_empty_string;
}
return value;
}
Vector<String> ManifestParser::ParseFileFilterAccept(const JSONObject* object) {
Vector<String> accept_types;
if (!object->Get("accept")) {
return accept_types;
}
String accept_str;
if (object->GetString("accept", &accept_str)) {
accept_types.push_back(accept_str);
return accept_types;
}
JSONArray* accept_list = object->GetArray("accept");
if (!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 JSONValue& accept_value : *accept_list) {
String accept_string;
if (!accept_value.AsString(&accept_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(accept_string);
}
return accept_types;
}
Vector<mojom::blink::ManifestFileFilterPtr> ManifestParser::ParseTargetFiles(
const String& key,
const JSONObject* from) {
Vector<mojom::blink::ManifestFileFilterPtr> files;
if (!from->Get(key)) {
return files;
}
JSONArray* file_list = from->GetArray(key);
if (!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) FileFilter.
const JSONObject* file_object = from->GetJSONObject(key);
if (!file_object) {
AddErrorInfo(
"property 'files' ignored, type array or FileFilter expected.");
return files;
}
ParseFileFilter(file_object, &files);
return files;
}
for (const JSONValue& value : *file_list) {
auto* file_object = JSONObject::Cast(&value);
if (!file_object) {
AddErrorInfo("files must be a sequence of non-empty file entries.");
continue;
}
ParseFileFilter(file_object, &files);
}
return files;
}
void ManifestParser::ParseFileFilter(
const JSONObject* file_object,
Vector<mojom::blink::ManifestFileFilterPtr>* files) {
auto file = mojom::blink::ManifestFileFilter::New();
file->name = ParseFileFilterName(file_object);
if (file->name.empty()) {
// https://wicg.github.io/web-share-target/level-2/#share_target-member
// step 7.1 requires that we invalidate this FileFilter if 'name' is an
// empty string. We also invalidate if 'name' is undefined or not a
// string.
return;
}
file->accept = ParseFileFilterAccept(file_object);
if (file->accept.empty()) {
return;
}
files->push_back(std::move(file));
}
std::optional<mojom::blink::ManifestShareTarget::Method>
ManifestParser::ParseShareTargetMethod(const JSONObject* share_target_object) {
if (!share_target_object->Get("method")) {
AddErrorInfo(
"Method should be set to either GET or POST. It currently defaults to "
"GET.");
return mojom::blink::ManifestShareTarget::Method::kGet;
}
String value;
if (!share_target_object->GetString("method", &value)) {
return std::nullopt;
}
String method = value.UpperASCII();
if (method == "GET") {
return mojom::blink::ManifestShareTarget::Method::kGet;
}
if (method == "POST") {
return mojom::blink::ManifestShareTarget::Method::kPost;
}
return std::nullopt;
}
std::optional<mojom::blink::ManifestShareTarget::Enctype>
ManifestParser::ParseShareTargetEnctype(const JSONObject* share_target_object) {
if (!share_target_object->Get("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 mojom::blink::ManifestShareTarget::Enctype::kFormUrlEncoded;
}
String value;
if (!share_target_object->GetString("enctype", &value)) {
return std::nullopt;
}
String enctype = value.LowerASCII();
if (enctype == "application/x-www-form-urlencoded") {
return mojom::blink::ManifestShareTarget::Enctype::kFormUrlEncoded;
}
if (enctype == "multipart/form-data") {
return mojom::blink::ManifestShareTarget::Enctype::kMultipartFormData;
}
return std::nullopt;
}
mojom::blink::ManifestShareTargetParamsPtr
ManifestParser::ParseShareTargetParams(const JSONObject* share_target_params) {
auto params = mojom::blink::ManifestShareTargetParams::New();
// NOTE: These are key names for query parameters, which are filled with share
// data. As such, |params.url| is just a string.
std::optional<String> text =
ParseString(share_target_params, "text", Trim(true));
params->text = text.has_value() ? *text : String();
std::optional<String> title =
ParseString(share_target_params, "title", Trim(true));
params->title = title.has_value() ? *title : String();
std::optional<String> url =
ParseString(share_target_params, "url", Trim(true));
params->url = url.has_value() ? *url : String();
auto files = ParseTargetFiles("files", share_target_params);
if (!files.empty()) {
params->files = std::move(files);
}
return params;
}
std::optional<mojom::blink::ManifestShareTargetPtr>
ManifestParser::ParseShareTarget(const JSONObject* object) {
const JSONObject* share_target_object = object->GetJSONObject("share_target");
if (!share_target_object) {
return std::nullopt;
}
auto share_target = mojom::blink::ManifestShareTarget::New();
share_target->action = ParseURL(share_target_object, "action", manifest_url_,
ParseURLRestrictions::kWithinScope);
if (!share_target->action.IsValid()) {
AddErrorInfo(
"property 'share_target' ignored. Property 'action' is "
"invalid.");
return std::nullopt;
}
auto method = ParseShareTargetMethod(share_target_object);
auto enctype = ParseShareTargetEnctype(share_target_object);
const JSONObject* share_target_params_object =
share_target_object->GetJSONObject("params");
if (!share_target_params_object) {
AddErrorInfo(
"property 'share_target' ignored. Property 'params' type "
"dictionary expected.");
return std::nullopt;
}
share_target->params = ParseShareTargetParams(share_target_params_object);
if (!method.has_value()) {
AddErrorInfo(
"invalid method. Allowed methods are:"
"GET and POST.");
return std::nullopt;
}
share_target->method = method.value();
if (!enctype.has_value()) {
AddErrorInfo(
"invalid enctype. Allowed enctypes are:"
"application/x-www-form-urlencoded and multipart/form-data.");
return std::nullopt;
}
share_target->enctype = enctype.value();
if (share_target->method == mojom::blink::ManifestShareTarget::Method::kGet) {
if (share_target->enctype ==
mojom::blink::ManifestShareTarget::Enctype::kMultipartFormData) {
AddErrorInfo(
"invalid enctype for GET method. Only "
"application/x-www-form-urlencoded is allowed.");
return std::nullopt;
}
}
if (share_target->params->files.has_value()) {
if (share_target->method !=
mojom::blink::ManifestShareTarget::Method::kPost ||
share_target->enctype !=
mojom::blink::ManifestShareTarget::Enctype::kMultipartFormData) {
AddErrorInfo("files are only supported with multipart/form-data POST.");
return std::nullopt;
}
}
if (share_target->params->files.has_value() &&
!VerifyFiles(*share_target->params->files)) {
AddErrorInfo("invalid mime type inside files.");
return std::nullopt;
}
return share_target;
}
Vector<mojom::blink::ManifestFileHandlerPtr> ManifestParser::ParseFileHandlers(
const JSONObject* object) {
if (!object->Get("file_handlers")) {
return {};
}
JSONArray* entry_array = object->GetArray("file_handlers");
if (!entry_array) {
AddErrorInfo("property 'file_handlers' ignored, type array expected.");
return {};
}
Vector<mojom::blink::ManifestFileHandlerPtr> result;
for (const JSONValue& value : *entry_array) {
auto* json_entry = JSONObject::Cast(&value);
if (!json_entry) {
AddErrorInfo("FileHandler ignored, type object expected.");
continue;
}
std::optional<mojom::blink::ManifestFileHandlerPtr> entry =
ParseFileHandler(json_entry);
if (!entry) {
continue;
}
result.push_back(std::move(entry.value()));
}
return result;
}
std::optional<mojom::blink::ManifestFileHandlerPtr>
ManifestParser::ParseFileHandler(const JSONObject* file_handler) {
mojom::blink::ManifestFileHandlerPtr entry =
mojom::blink::ManifestFileHandler::New();
entry->action = ParseURL(file_handler, "action", manifest_url_,
ParseURLRestrictions::kWithinScope);
if (!entry->action.IsValid()) {
AddErrorInfo("FileHandler ignored. Property 'action' is invalid.");
return std::nullopt;
}
entry->name = ParseString(file_handler, "name", Trim(true)).value_or("");
const bool feature_enabled =
base::FeatureList::IsEnabled(blink::features::kFileHandlingIcons) ||
RuntimeEnabledFeatures::FileHandlingIconsEnabled(execution_context_);
if (feature_enabled) {
entry->icons = ParseIcons(file_handler);
}
entry->accept = ParseFileHandlerAccept(file_handler->GetJSONObject("accept"));
if (entry->accept.empty()) {
AddErrorInfo("FileHandler ignored. Property 'accept' is invalid.");
return std::nullopt;
}
entry->launch_type =
ParseFirstValidEnum<
std::optional<mojom::blink::ManifestFileHandler::LaunchType>>(
file_handler, "launch_type", &FileHandlerLaunchTypeFromString,
/*invalid_value=*/std::nullopt)
.value_or(
mojom::blink::ManifestFileHandler::LaunchType::kSingleClient);
return entry;
}
HashMap<String, Vector<String>> ManifestParser::ParseFileHandlerAccept(
const JSONObject* accept) {
HashMap<String, Vector<String>> result;
if (!accept) {
return result;
}
const int kExtensionLimit = g_file_handler_extension_limit_for_testing > 0
? g_file_handler_extension_limit_for_testing
: kFileHandlerExtensionLimit;
if (total_file_handler_extension_count_ > kExtensionLimit) {
return result;
}
for (wtf_size_t i = 0; i < accept->size(); ++i) {
JSONObject::Entry entry = accept->at(i);
// Validate the MIME type.
String& mimetype = entry.first;
std::string top_level_mime_type;
if (!net::ParseMimeTypeWithoutParameter(mimetype.Utf8(),
&top_level_mime_type, nullptr) ||
!net::IsValidTopLevelMimeType(top_level_mime_type)) {
AddErrorInfo(StrCat({"invalid MIME type: ", mimetype}));
continue;
}
Vector<String> extensions;
String extension;
JSONArray* extensions_array = JSONArray::Cast(entry.second);
if (extensions_array) {
for (const JSONValue& value : *extensions_array) {
if (!value.AsString(&extension)) {
AddErrorInfo(
"property 'accept' file extension ignored, type string "
"expected.");
continue;
}
if (!ParseFileHandlerAcceptExtension(&value, &extension)) {
// Errors are added by ParseFileHandlerAcceptExtension.
continue;
}
extensions.push_back(extension);
}
} else if (ParseFileHandlerAcceptExtension(entry.second, &extension)) {
extensions.push_back(extension);
} else {
// Parsing errors will already have been added.
continue;
}
total_file_handler_extension_count_ += extensions.size();
int extension_overflow =
total_file_handler_extension_count_ - kExtensionLimit;
if (extension_overflow > 0) {
auto erase_iter = UNSAFE_TODO(extensions.end() - extension_overflow);
AddErrorInfo(
StrCat({"property 'accept': too many total file extensions, ignoring "
"extensions starting from \"",
*erase_iter, "\""}));
extensions.erase(erase_iter, extensions.end());
}
if (!extensions.empty()) {
result.Set(mimetype, std::move(extensions));
}
if (extension_overflow > 0) {
break;
}
}
return result;
}
bool ManifestParser::ParseFileHandlerAcceptExtension(const JSONValue* extension,
String* output) {
if (!extension->AsString(output)) {
AddErrorInfo(
"property 'accept' type ignored. File extensions must be type array or "
"type string.");
return false;
}
if (!output->StartsWith(".")) {
AddErrorInfo(
"property 'accept' file extension ignored, must start with a '.'.");
return false;
}
return true;
}
Vector<mojom::blink::ManifestProtocolHandlerPtr>
ManifestParser::ParseProtocolHandlers(const JSONObject* from) {
Vector<mojom::blink::ManifestProtocolHandlerPtr> protocols;
if (!from->Get("protocol_handlers")) {
return protocols;
}
JSONArray* protocol_list = from->GetArray("protocol_handlers");
if (!protocol_list) {
AddErrorInfo("property 'protocol_handlers' ignored, type array expected.");
return protocols;
}
for (const JSONValue& value : *protocol_list) {
auto* protocol_object = JSONObject::Cast(&value);
if (!protocol_object) {
AddErrorInfo("protocol_handlers entry ignored, type object expected.");
continue;
}
std::optional<mojom::blink::ManifestProtocolHandlerPtr> protocol =
ParseProtocolHandler(protocol_object);
if (!protocol) {
continue;
}
protocols.push_back(std::move(protocol.value()));
}
return protocols;
}
std::optional<mojom::blink::ManifestProtocolHandlerPtr>
ManifestParser::ParseProtocolHandler(const JSONObject* object) {
if (!object->Get("protocol")) {
AddErrorInfo(
"protocol_handlers entry ignored, required property 'protocol' is "
"missing.");
return std::nullopt;
}
auto protocol_handler = mojom::blink::ManifestProtocolHandler::New();
std::optional<String> protocol = ParseString(object, "protocol", Trim(true));
String error_message;
bool is_valid_protocol = protocol.has_value();
ProtocolHandlerSecurityLevel security_level =
execution_context_->IsIsolatedContext()
? ProtocolHandlerSecurityLevel::kIsolatedAppFeatures
: ProtocolHandlerSecurityLevel::kStrict;
if (is_valid_protocol &&
!VerifyCustomHandlerScheme(protocol.value(), error_message,
security_level)) {
AddErrorInfo(error_message);
is_valid_protocol = false;
}
if (!is_valid_protocol) {
AddErrorInfo(
"protocol_handlers entry ignored, required property 'protocol' is "
"invalid.");
return std::nullopt;
}
protocol_handler->protocol = protocol.value();
if (!object->Get("url")) {
AddErrorInfo(
"protocol_handlers entry ignored, required property 'url' is missing.");
return std::nullopt;
}
protocol_handler->url = ParseURL(object, "url", manifest_url_,
ParseURLRestrictions::kWithinScope);
bool is_valid_url = protocol_handler->url.IsValid();
if (is_valid_url) {
const char kToken[] = "%s";
String user_url = protocol_handler->url.GetString();
String tokenless_url = protocol_handler->url.GetString();
tokenless_url.Remove(user_url.Find(kToken), std::size(kToken) - 1);
KURL full_url(manifest_url_, tokenless_url);
if (!VerifyCustomHandlerURLSyntax(full_url, manifest_url_, user_url,
security_level, error_message)) {
AddErrorInfo(error_message);
is_valid_url = false;
}
}
if (!is_valid_url) {
AddErrorInfo(
"protocol_handlers entry ignored, required property 'url' is invalid.");
return std::nullopt;
}
return std::move(protocol_handler);
}
Vector<mojom::blink::ManifestScopeExtensionPtr>
ManifestParser::ParseScopeExtensions(const JSONObject* from) {
Vector<mojom::blink::ManifestScopeExtensionPtr> scope_extensions;
if (!from->Get("scope_extensions")) {
return scope_extensions;
}
JSONArray* extensions_list = from->GetArray("scope_extensions");
if (!extensions_list) {
AddErrorInfo("property 'scope_extensions' ignored, type array expected.");
return scope_extensions;
}
for (wtf_size_t i = 0; i < extensions_list->size(); ++i) {
if (i == kMaxScopeExtensionsSize) {
AddErrorInfo(
StrCat({"property 'scope_extensions' contains more than ",
String::Number(kMaxScopeExtensionsSize),
" valid elements, only the first ",
String::Number(kMaxScopeExtensionsSize), " are parsed."}));
break;
}
const JSONValue* extensions_entry = extensions_list->at(i);
if (!extensions_entry) {
AddErrorInfo("scope_extensions entry ignored, entry is null.");
continue;
}
JSONValue::ValueType entry_type = extensions_entry->GetType();
if (entry_type != JSONValue::kTypeObject) {
AddErrorInfo("scope_extensions entry ignored, type object expected.");
continue;
}
std::optional<mojom::blink::ManifestScopeExtensionPtr> scope_extension =
std::nullopt;
const JSONObject* extension_object = JSONObject::Cast(extensions_entry);
if (!extension_object) {
AddErrorInfo("scope_extensions entry ignored, type object expected.");
continue;
}
scope_extension = ParseScopeExtension(extension_object);
if (!scope_extension) {
continue;
}
scope_extensions.push_back(std::move(scope_extension.value()));
}
return scope_extensions;
}
std::optional<mojom::blink::ManifestScopeExtensionPtr>
ManifestParser::ParseScopeExtension(const JSONObject* object) {
if (!object->Get(kScopeExtensionsTypeKey) ||
!object->Get(kScopeExtensionsOriginKey)) {
AddErrorInfo(kScopeExtensionsMissingKeysErrorMessage);
return std::nullopt;
}
const std::optional<String> scope_extension_type =
ParseString(object, kScopeExtensionsTypeKey, Trim(true));
if (!scope_extension_type.has_value()) {
AddErrorInfo("Scope extension 'type' invalid.");
return std::nullopt;
}
if (scope_extension_type.value() ==
ScopeExtensionTypeMap[static_cast<int>(ScopeExtensionType::kOrigin)]) {
const std::optional<String> origin_string =
ParseString(object, kScopeExtensionsOriginKey, Trim(true));
if (!origin_string.has_value()) {
AddErrorInfo("Scope extension 'origin' invalid.");
return std::nullopt;
}
return ParseScopeExtensionOrigin(*origin_string);
} else {
AddErrorInfo("Scope extension 'type' invalid.");
return std::nullopt;
}
}
std::optional<mojom::blink::ManifestScopeExtensionPtr>
ManifestParser::ParseScopeExtensionOrigin(const String& origin_string) {
// TODO(crbug.com/1250011): pre-process for input without scheme.
// (eg. example.com instead of https://example.com) because we can always
// assume the use of https for scope extensions. Remove this TODO if we decide
// to require fully specified https scheme in this origin input.
if (origin_string.length() > kMaxOriginLength) {
AddErrorInfo(
StrCat({"scope_extensions entry ignored, 'origin' exceeds maximum "
"character length of ",
String::Number(kMaxOriginLength), " ."}));
return std::nullopt;
}
auto origin = SecurityOrigin::CreateFromString(origin_string);
if (!origin || origin->IsOpaque()) {
AddErrorInfo(
"scope_extensions entry ignored, required property 'origin' is "
"invalid.");
return std::nullopt;
}
if (origin->Protocol() != url::kHttpsScheme) {
AddErrorInfo(
"scope_extensions entry ignored, required property 'origin' must use "
"the https scheme.");
return std::nullopt;
}
String host = origin->Host();
auto scope_extension = mojom::blink::ManifestScopeExtension::New();
// Check for wildcard *.
if (base::FeatureList::IsEnabled(
blink::features::kWebAppEnableScopeExtensionsBySite) &&
host.StartsWith(kOriginWildcardPrefix)) {
scope_extension->has_origin_wildcard = true;
// Trim the wildcard prefix to get the effective host. Minus one to exclude
// the length of the null terminator.
host = host.Substring(sizeof(kOriginWildcardPrefix) - 1);
} else {
scope_extension->has_origin_wildcard = false;
}
bool host_valid = IsHostValidForScopeExtension(host);
if (!host_valid) {
AddErrorInfo(
"scope_extensions entry ignored, domain of required property 'origin' "
"is invalid.");
return std::nullopt;
}
if (scope_extension->has_origin_wildcard) {
origin = SecurityOrigin::CreateFromValidTuple(origin->Protocol(), host,
origin->Port());
if (!origin) {
AddErrorInfo(
"scope_extensions entry ignored, required property 'origin' is "
"invalid.");
return std::nullopt;
}
}
scope_extension->origin = origin;
return std::move(scope_extension);
}
KURL ManifestParser::ParseLockScreenStartUrl(const JSONObject* lock_screen) {
if (!lock_screen->Get("start_url")) {
return KURL();
}
KURL start_url = ParseURL(lock_screen, "start_url", manifest_url_,
ParseURLRestrictions::kWithinScope);
if (!start_url.IsValid()) {
// Error already reported by ParseURL.
return KURL();
}
return start_url;
}
mojom::blink::ManifestLockScreenPtr ManifestParser::ParseLockScreen(
const JSONObject* manifest) {
if (!manifest->Get("lock_screen")) {
return nullptr;
}
const JSONObject* lock_screen_object = manifest->GetJSONObject("lock_screen");
if (!lock_screen_object) {
AddErrorInfo("property 'lock_screen' ignored, type object expected.");
return nullptr;
}
auto lock_screen = mojom::blink::ManifestLockScreen::New();
lock_screen->start_url = ParseLockScreenStartUrl(lock_screen_object);
return lock_screen;
}
KURL ManifestParser::ParseNoteTakingNewNoteUrl(const JSONObject* note_taking) {
if (!note_taking->Get("new_note_url")) {
return KURL();
}
KURL new_note_url = ParseURL(note_taking, "new_note_url", manifest_url_,
ParseURLRestrictions::kWithinScope);
if (!new_note_url.IsValid()) {
// Error already reported by ParseURL.
return KURL();
}
return new_note_url;
}
mojom::blink::ManifestNoteTakingPtr ManifestParser::ParseNoteTaking(
const JSONObject* manifest) {
if (!manifest->Get("note_taking")) {
return nullptr;
}
const JSONObject* note_taking_object = manifest->GetJSONObject("note_taking");
if (!note_taking_object) {
AddErrorInfo("property 'note_taking' ignored, type object expected.");
return nullptr;
}
auto note_taking = mojom::blink::ManifestNoteTaking::New();
note_taking->new_note_url = ParseNoteTakingNewNoteUrl(note_taking_object);
return note_taking;
}
String ManifestParser::ParseRelatedApplicationPlatform(
const JSONObject* application) {
std::optional<String> platform =
ParseString(application, "platform", Trim(true));
return platform.has_value() ? *platform : String();
}
std::optional<KURL> ManifestParser::ParseRelatedApplicationURL(
const JSONObject* application) {
return ParseURL(application, "url", manifest_url_,
ParseURLRestrictions::kNoRestrictions);
}
String ManifestParser::ParseRelatedApplicationId(
const JSONObject* application) {
std::optional<String> id = ParseString(application, "id", Trim(true));
return id.has_value() ? *id : String();
}
Vector<mojom::blink::ManifestRelatedApplicationPtr>
ManifestParser::ParseRelatedApplications(const JSONObject* object) {
Vector<mojom::blink::ManifestRelatedApplicationPtr> applications;
JSONValue* value = object->Get("related_applications");
if (!value) {
return applications;
}
JSONArray* applications_list = object->GetArray("related_applications");
if (!applications_list) {
AddErrorInfo(
"property 'related_applications' ignored,"
" type array expected.");
return applications;
}
for (const JSONValue& entry : *applications_list) {
auto* application_object = JSONObject::Cast(&entry);
if (!application_object) {
continue;
}
auto application = mojom::blink::ManifestRelatedApplication::New();
application->platform = ParseRelatedApplicationPlatform(application_object);
// "If platform is undefined, move onto the next item if any are left."
if (application->platform.empty()) {
AddErrorInfo(
"'platform' is a required field, related application"
" ignored.");
continue;
}
application->id = ParseRelatedApplicationId(application_object);
application->url = ParseRelatedApplicationURL(application_object);
// "If both id and url are undefined, move onto the next item if any are
// left."
if ((!application->url.has_value() || !application->url->IsValid()) &&
application->id.empty()) {
AddErrorInfo(
"one of 'url' or 'id' is required, related application"
" ignored.");
continue;
}
applications.push_back(std::move(application));
}
return applications;
}
bool ManifestParser::ParsePreferRelatedApplications(const JSONObject* object) {
return ParseBoolean(object, "prefer_related_applications", false);
}
std::optional<RGBA32> ManifestParser::ParseThemeColor(
const JSONObject* object) {
return ParseColor(object, "theme_color");
}
std::optional<RGBA32> ManifestParser::ParseBackgroundColor(
const JSONObject* object) {
return ParseColor(object, "background_color");
}
String ManifestParser::ParseGCMSenderID(const JSONObject* object) {
std::optional<String> gcm_sender_id =
ParseString(object, "gcm_sender_id", Trim(true));
return gcm_sender_id.has_value() ? *gcm_sender_id : String();
}
Vector<network::ParsedPermissionsPolicyDeclaration>
ManifestParser::ParseIsolatedAppPermissions(const JSONObject* object) {
PermissionsPolicyParser::Node policy{
network::OriginWithPossibleWildcards::NodeType::kHeader};
JSONValue* json_value = object->Get("permissions_policy");
if (!json_value) {
return Vector<network::ParsedPermissionsPolicyDeclaration>();
}
JSONObject* permissions_dict = object->GetJSONObject("permissions_policy");
if (!permissions_dict) {
AddErrorInfo(
"property 'permissions_policy' ignored, type object expected.");
return Vector<network::ParsedPermissionsPolicyDeclaration>();
}
for (wtf_size_t i = 0; i < permissions_dict->size(); ++i) {
const JSONObject::Entry& entry = permissions_dict->at(i);
String feature(entry.first);
JSONArray* origin_allowlist = JSONArray::Cast(entry.second);
if (!origin_allowlist) {
AddErrorInfo(
StrCat({"permission '", feature,
"' ignored, invalid allowlist: type array expected."}));
continue;
}
Vector<String> allowlist = ParseOriginAllowlist(origin_allowlist, feature);
if (!allowlist.size()) {
continue;
}
PermissionsPolicyParser::Declaration new_policy;
new_policy.feature_name = feature;
for (const auto& origin : allowlist) {
// PermissionsPolicyParser expects 4 types of origin strings:
// - "self": wrapped in single quotes (as in a header)
// - "none": wrapped in single quotes (as in a header)
// - "*" (asterisk): not wrapped
// - "<origin>": actual origin names should not be wrapped in single
// quotes
// The "src" origin string type can be ignored here as it's only used in
// the iframe "allow" attribute.
//
// Sidenote: Actual origin names ("<origin>") are parsed using
// OriginWithPossibleWildcards::Parse() which fails if the origin string
// contains any non-alphanumeric characters, such as a single quote. For
// this reason, actual origin names must not be wrapped since the parser
// will just drop them as being improperly formatted (i.e. they would be
// the equivalent to some manifest containing an origin wrapped in single
// quotes, which is invalid).
String wrapped_origin = origin;
if (EqualIgnoringASCIICase(origin, "self") ||
EqualIgnoringASCIICase(origin, "none")) {
wrapped_origin = StrCat({"'", origin, "'"});
;
}
new_policy.allowlist.push_back(wrapped_origin);
}
policy.declarations.push_back(new_policy);
}
PolicyParserMessageBuffer logger(
"Error with permissions_policy manifest field: ");
network::ParsedPermissionsPolicy parsed_policy =
PermissionsPolicyParser::ParsePolicyFromNode(
policy, SecurityOrigin::Create(manifest_url_), logger,
execution_context_);
Vector<network::ParsedPermissionsPolicyDeclaration> out;
for (const auto& decl : parsed_policy) {
out.push_back(std::move(decl));
}
return out;
}
Vector<String> ManifestParser::ParseOriginAllowlist(
const JSONArray* json_allowlist,
const String& feature) {
Vector<String> out;
for (const JSONValue& json_value : *json_allowlist) {
String origin_string;
if (!json_value.AsString(&origin_string) || origin_string.IsNull()) {
AddErrorInfo(
"permissions_policy entry ignored, required property 'origin' "
"contains "
"an invalid element: type string expected.");
return Vector<String>();
}
if (!origin_string.length()) {
AddErrorInfo(
"permissions_policy entry ignored, required property 'origin' is "
"contains an empty string.");
return Vector<String>();
}
if (origin_string.length() > kMaxOriginLength) {
AddErrorInfo(
StrCat({"permissions_policy entry ignored, 'origin' exceeds maximum "
"character length of ",
String::Number(kMaxOriginLength), " ."}));
return Vector<String>();
}
out.push_back(origin_string);
}
return out;
}
mojom::blink::ManifestLaunchHandlerPtr ManifestParser::ParseLaunchHandler(
const JSONObject* object) {
const JSONValue* launch_handler_value = object->Get("launch_handler");
if (!launch_handler_value) {
return nullptr;
}
const JSONObject* launch_handler_object =
JSONObject::Cast(launch_handler_value);
if (!launch_handler_object) {
AddErrorInfo("launch_handler value ignored, object expected.");
return nullptr;
}
using ClientMode = mojom::blink::ManifestLaunchHandler::ClientMode;
return mojom::blink::ManifestLaunchHandler::New(
ParseFirstValidEnum<std::optional<ClientMode>>(
launch_handler_object, "client_mode", &ClientModeFromString,
/*invalid_value=*/std::nullopt));
}
HashMap<String, mojom::blink::ManifestTranslationItemPtr>
ManifestParser::ParseTranslations(const JSONObject* object) {
HashMap<String, mojom::blink::ManifestTranslationItemPtr> result;
if (!object->Get("translations")) {
return result;
}
JSONObject* translations_map = object->GetJSONObject("translations");
if (!translations_map) {
AddErrorInfo("property 'translations' ignored, object expected.");
return result;
}
for (wtf_size_t i = 0; i < translations_map->size(); ++i) {
JSONObject::Entry entry = translations_map->at(i);
String locale = entry.first;
if (locale == "") {
AddErrorInfo("skipping translation, non-empty locale string expected.");
continue;
}
JSONObject* translation = JSONObject::Cast(entry.second);
if (!translation) {
AddErrorInfo("skipping translation, object expected.");
continue;
}
auto translation_item = mojom::blink::ManifestTranslationItem::New();
std::optional<String> name = ParseStringForMember(
translation, "translations", "name", false, Trim(true));
translation_item->name =
name.has_value() && name->length() != 0 ? *name : String();
std::optional<String> short_name = ParseStringForMember(
translation, "translations", "short_name", false, Trim(true));
translation_item->short_name =
short_name.has_value() && short_name->length() != 0 ? *short_name
: String();
std::optional<String> description = ParseStringForMember(
translation, "translations", "description", false, Trim(true));
translation_item->description =
description.has_value() && description->length() != 0 ? *description
: String();
// A translation may be specified for any combination of translatable fields
// in the manifest. If no translations are supplied, we skip this item.
if (!translation_item->name && !translation_item->short_name &&
!translation_item->description) {
continue;
}
result.Set(locale, std::move(translation_item));
}
return result;
}
mojom::blink::ManifestTabStripPtr ManifestParser::ParseTabStrip(
const JSONObject* object) {
if (!object->Get("tab_strip")) {
return nullptr;
}
JSONObject* tab_strip_object = object->GetJSONObject("tab_strip");
if (!tab_strip_object) {
AddErrorInfo("property 'tab_strip' ignored, object expected.");
return nullptr;
}
auto result = mojom::blink::ManifestTabStrip::New();
JSONValue* home_tab_value = tab_strip_object->Get("home_tab");
if (home_tab_value && home_tab_value->GetType() == JSONValue::kTypeObject) {
JSONObject* home_tab_object = tab_strip_object->GetJSONObject("home_tab");
auto home_tab_params = mojom::blink::HomeTabParams::New();
JSONValue* home_tab_icons = home_tab_object->Get("icons");
String string_value;
if (home_tab_icons && !(home_tab_icons->AsString(&string_value) &&
EqualIgnoringASCIICase(string_value, "auto"))) {
home_tab_params->icons = ParseIcons(home_tab_object);
}
home_tab_params->scope_patterns =
ParseUrlPatterns(home_tab_object, "scope_patterns");
result->home_tab =
mojom::blink::HomeTabUnion::NewParams(std::move(home_tab_params));
} else {
result->home_tab = mojom::blink::HomeTabUnion::NewVisibility(
ParseTabStripMemberVisibility(home_tab_value));
}
auto new_tab_button_params = mojom::blink::NewTabButtonParams::New();
JSONObject* new_tab_button_object =
tab_strip_object->GetJSONObject("new_tab_button");
if (new_tab_button_object) {
JSONValue* new_tab_button_url = new_tab_button_object->Get("url");
String string_value;
if (new_tab_button_url && !(new_tab_button_url->AsString(&string_value) &&
EqualIgnoringASCIICase(string_value, "auto"))) {
KURL url = ParseURL(new_tab_button_object, "url", manifest_url_,
ParseURLRestrictions::kWithinScope);
if (!url.IsNull()) {
new_tab_button_params->url = url;
}
}
}
result->new_tab_button = std::move(new_tab_button_params);
return result;
}
mojom::blink::TabStripMemberVisibility
ManifestParser::ParseTabStripMemberVisibility(const JSONValue* json_value) {
if (!json_value) {
return mojom::blink::TabStripMemberVisibility::kAuto;
}
String string_value;
if (json_value->AsString(&string_value) &&
EqualIgnoringASCIICase(string_value, "absent")) {
return mojom::blink::TabStripMemberVisibility::kAbsent;
}
return mojom::blink::TabStripMemberVisibility::kAuto;
}
Vector<SafeUrlPattern> ManifestParser::ParseUrlPatterns(
const JSONObject* object,
const String& field_name) {
Vector<SafeUrlPattern> result;
auto* url_patterns = object->GetArray(field_name);
if (!url_patterns) {
return result;
}
for (const JSONValue& entry : *url_patterns) {
// TODO(b/330640840): allow strings to be passed through here and parsed via
// liburlpattern::ConstructorStringParser. The result of the parse can then
// be used to create a PatternInit object for the rest of the process.
auto* pattern_object = JSONObject::Cast(&entry);
if (!pattern_object) {
continue;
}
std::optional<PatternInit> init = MaybeCreatePatternInit(pattern_object);
if (init.has_value()) {
auto base_url = init->base_url.IsValid() ? init->base_url : manifest_url_;
std::optional<SafeUrlPattern> pattern =
ParseUrlPattern(field_name, init.value(), base_url);
if (pattern.has_value()) {
result.push_back(std::move(pattern.value()));
}
}
}
return result;
}
std::optional<SafeUrlPattern> ManifestParser::ParseUrlPattern(
const String& field_name,
const PatternInit& init,
const KURL& base_url) {
auto url_pattern = std::make_optional<SafeUrlPattern>();
{
// https://urlpattern.spec.whatwg.org/#process-a-urlpatterninit
// Always fall back to baseURL protocol if init does not contain protocol.
std::optional<std::vector<liburlpattern::Part>> part_list =
ParsePatternInitField(init.protocol, base_url.Protocol());
if (!part_list.has_value()) {
AddErrorInfo(
StrCat({"property 'protocol' in '", field_name,
"' pattern could not be parsed or contains banned regex."}));
return std::nullopt;
}
url_pattern->protocol = std::move(part_list.value());
}
{
// https://urlpattern.spec.whatwg.org/#process-a-urlpatterninit
// Only fall back to baseURL username if init does not contain any of
// protocol, hostname, or port.
String default_username;
if (!init.IsAbsolute()) {
default_username = base_url.User().ToString();
}
std::optional<std::vector<liburlpattern::Part>> part_list =
ParsePatternInitField(init.username, default_username);
if (!part_list.has_value()) {
AddErrorInfo(
StrCat({"property 'username' in '", field_name,
"' pattern could not be parsed or contains banned regex."}));
return std::nullopt;
}
url_pattern->username = std::move(part_list.value());
}
{
// https://urlpattern.spec.whatwg.org/#process-a-urlpatterninit
// Only fall back to baseURL password if init does not contain any of
// protocol, hostname, port, or username.
String default_password;
if (!init.IsAbsolute() && !init.username.has_value()) {
default_password = base_url.Pass().ToString();
}
std::optional<std::vector<liburlpattern::Part>> part_list =
ParsePatternInitField(init.password, default_password);
if (!part_list.has_value()) {
AddErrorInfo(
StrCat({"property 'password' in '", field_name,
"' pattern could not be parsed or contains banned regex."}));
return std::nullopt;
}
url_pattern->password = std::move(part_list.value());
}
{
// https://urlpattern.spec.whatwg.org/#process-a-urlpatterninit
// Only fall back to baseURL hostname if init does not contain protocol.
String default_hostname;
if (!init.protocol.has_value()) {
default_hostname = base_url.Host().ToString();
}
std::optional<std::vector<liburlpattern::Part>> part_list =
ParsePatternInitField(init.hostname, default_hostname);
if (!part_list.has_value()) {
AddErrorInfo(
StrCat({"property 'hostname' in '", field_name,
"' pattern could not be parsed or contains banned regex."}));
return std::nullopt;
}
url_pattern->hostname = std::move(part_list.value());
}
{
// https://urlpattern.spec.whatwg.org/#process-a-urlpatterninit
// Only fall back to baseURL port if init does not contain any of
// protocol, hostname, or port, and the baseURL port exists.
String default_port;
if (!init.IsAbsolute() && base_url.HasPort()) {
default_port = String::Number(base_url.Port());
}
std::optional<std::vector<liburlpattern::Part>> part_list =
ParsePatternInitField(init.port, default_port);
if (!part_list.has_value()) {
AddErrorInfo(
StrCat({"property 'port' in '", field_name,
"' pattern could not be parsed or contains banned regex."}));
return std::nullopt;
}
url_pattern->port = std::move(part_list.value());
}
{
String default_path;
if (init.pathname.has_value()) {
// A possibly-relative path is given; resolve it against base URL's path.
default_path =
ResolveRelativePathnamePattern(base_url, init.pathname.value());
} else if (!init.IsAbsolute()) {
// No path, protocol, host or port is given; use the base URL's path.
default_path = EscapePatternString(base_url.GetPath());
}
// else: no path, but a protocol, host or port was given, making this
// pattern absolute, so treat the path as empty.
std::optional<std::vector<liburlpattern::Part>> part_list =
ParsePatternInitField(std::nullopt, default_path);
if (!part_list.has_value()) {
AddErrorInfo(
StrCat({"property 'pathname' in '", field_name,
"' pattern could not be parsed or contains banned regex."}));
return std::nullopt;
}
url_pattern->pathname = std::move(part_list.value());
}
{
// https://urlpattern.spec.whatwg.org/#process-a-urlpatterninit
// Only fall back to baseURL search if init does not contain any of
// protocol, hostname, port, or pathname.
String default_search;
if (!init.IsAbsolute() && !init.pathname.has_value()) {
default_search = base_url.Query().ToString();
}
std::optional<std::vector<liburlpattern::Part>> part_list =
ParsePatternInitField(init.search, default_search);
if (!part_list.has_value()) {
AddErrorInfo(
StrCat({"property 'search' in '", field_name,
"' pattern could not be parsed or contains banned regex."}));
return std::nullopt;
}
url_pattern->search = std::move(part_list.value());
}
{
// https://urlpattern.spec.whatwg.org/#process-a-urlpatterninit
// Only fall back to baseURL hash if init does not contain any of
// protocol, hostname, port, pathname, or search.
String default_hash;
if (!init.IsAbsolute() && !init.pathname.has_value() &&
!init.search.has_value()) {
default_hash = base_url.FragmentIdentifier().ToString();
}
std::optional<std::vector<liburlpattern::Part>> part_list =
ParsePatternInitField(init.hash, default_hash);
if (!part_list.has_value()) {
AddErrorInfo(
StrCat({"property 'hash' in '", field_name,
"' pattern could not be parsed or contains banned regex."}));
return std::nullopt;
}
url_pattern->hash = std::move(part_list.value());
}
return url_pattern;
}
std::optional<ManifestParser::PatternInit>
ManifestParser::MaybeCreatePatternInit(const JSONObject* pattern_object) {
std::optional<String> protocol = ParseStringForMember(
pattern_object, "scope_patterns", "protocol", false, Trim(true));
std::optional<String> username = ParseStringForMember(
pattern_object, "scope_patterns", "username", false, Trim(true));
std::optional<String> password = ParseStringForMember(
pattern_object, "scope_patterns", "password", false, Trim(true));
std::optional<String> hostname = ParseStringForMember(
pattern_object, "scope_patterns", "hostname", false, Trim(true));
std::optional<String> port = ParseStringForMember(
pattern_object, "scope_patterns", "port", false, Trim(true));
std::optional<String> pathname = ParseStringForMember(
pattern_object, "scope_patterns", "pathname", false, Trim(true));
std::optional<String> search = ParseStringForMember(
pattern_object, "scope_patterns", "search", false, Trim(true));
std::optional<String> hash = ParseStringForMember(
pattern_object, "scope_patterns", "hash", false, Trim(true));
KURL base_url;
if (pattern_object->Get("baseURL")) {
base_url = ParseURL(pattern_object, "baseURL", KURL(),
ParseURLRestrictions::kNoRestrictions);
if (!base_url.IsValid()) {
return std::nullopt;
}
}
return std::make_optional<PatternInit>(
std::move(protocol), std::move(username), std::move(password),
std::move(hostname), std::move(port), std::move(pathname),
std::move(search), std::move(hash), base_url);
}
String ManifestParser::ParseVersion(const JSONObject* object) {
return ParseString(object, "version", Trim(false)).value_or(String());
}
void ManifestParser::AddErrorInfo(const String& error_msg,
bool critical,
int error_line,
int error_column) {
mojom::blink::ManifestErrorPtr error = mojom::blink::ManifestError::New(
error_msg, critical, error_line, error_column);
errors_.push_back(std::move(error));
}
HashMap<String, mojom::blink::ManifestLocalizedTextObjectPtr>
ManifestParser::ParseLocalizedField(const JSONObject* object,
const String& field_name) {
JSONObject* localized_value = object->GetJSONObject(field_name);
HashMap<String, mojom::blink::ManifestLocalizedTextObjectPtr> result;
if (!localized_value) {
return result;
}
for (wtf_size_t i = 0; i < localized_value->size(); ++i) {
const JSONObject::Entry& entry = localized_value->at(i);
const String& key = entry.first;
const JSONValue* json_value = entry.second;
// Parse individual localized text object
if (!json_value) {
continue;
}
String value;
String lang;
// Default direction is "auto"
mojom::blink::Manifest::TextDirection dir =
mojom::blink::Manifest::TextDirection::kAuto;
if (json_value->AsString(&value)) {
value = value.StripWhiteSpace();
} else if (const JSONObject* obj = JSONObject::Cast(json_value)) {
// value (required)
if (obj->Get("value")) {
auto maybe = ParseStringForMember(obj, field_name, "value",
/*required=*/true, Trim(true));
if (maybe.has_value()) {
value = std::move(*maybe);
}
}
// lang (optional)
if (obj->Get("lang")) {
auto maybe = ParseStringForMember(obj, field_name, "lang",
/*required=*/false, Trim(true));
if (maybe.has_value()) {
lang = std::move(*maybe);
}
}
// dir (optional)
if (obj->Get("dir")) {
dir = ParseDir(obj);
}
}
// We must have a non-empty value.
if (value.empty()) {
continue;
}
// If no lang tag was specified for this entry, fall back to manifest-level
// lang.
std::optional<String> manifest_lang_opt =
ParseString(object, "lang", Trim(true));
String manifest_lang = manifest_lang_opt.value_or(String());
if (lang.empty() && !manifest_lang.empty()) {
lang = manifest_lang;
}
// Validate the language tag if present.
if (!lang.empty()) {
UErrorCode status = U_ZERO_ERROR;
StringUtf8Adaptor lang_utf8(lang);
icu::Locale::forLanguageTag(lang_utf8.AsStringView(), status);
if (U_FAILURE(status)) {
AddErrorInfo(StrCat({"property '", field_name, "' entry for '", key,
"' ignored, invalid language tag '", lang, "'."}));
continue;
}
}
// Build and add the localized text object.
auto localized_text = mojom::blink::ManifestLocalizedTextObject::New();
localized_text->value = std::move(value);
localized_text->lang = std::move(lang);
localized_text->dir = dir;
result.Set(key, std::move(localized_text));
}
return result;
}
HashMap<String, mojom::blink::ManifestLocalizedTextObjectPtr>
ManifestParser::ParseNameLocalized(const JSONObject* object) {
return ParseLocalizedField(object, "name_localized");
}
HashMap<String, mojom::blink::ManifestLocalizedTextObjectPtr>
ManifestParser::ParseShortNameLocalized(const JSONObject* object) {
return ParseLocalizedField(object, "short_name_localized");
}
HashMap<String, mojom::blink::ManifestLocalizedTextObjectPtr>
ManifestParser::ParseDescriptionLocalized(const JSONObject* object) {
return ParseLocalizedField(object, "description_localized");
}
} // namespace blink