| // 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 <string> |
| |
| #include "base/feature_list.h" |
| #include "net/base/mime_util.h" |
| #include "net/base/registry_controlled_domains/registry_controlled_domain.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/manifest/manifest.h" |
| #include "third_party/blink/public/common/manifest/manifest_util.h" |
| #include "third_party/blink/public/common/mime_util/mime_util.h" |
| #include "third_party/blink/public/common/permissions_policy/origin_with_possible_wildcards.h" |
| #include "third_party/blink/public/common/permissions_policy/permissions_policy.h" |
| #include "third_party/blink/public/common/security/protocol_handler_security_level.h" |
| #include "third_party/blink/public/common/url_pattern.h" |
| #include "third_party/blink/public/mojom/manifest/manifest.mojom-blink-forward.h" |
| #include "third_party/blink/public/mojom/manifest/manifest.mojom-blink.h" |
| #include "third_party/blink/public/mojom/permissions_policy/permissions_policy.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/media_query_evaluator.h" |
| #include "third_party/blink/renderer/core/css/media_values.h" |
| #include "third_party/blink/renderer/core/css/media_values_cached.h" |
| #include "third_party/blink/renderer/core/css/parser/css_parser.h" |
| #include "third_party/blink/renderer/core/css/parser/css_parser_token_range.h" |
| #include "third_party/blink/renderer/core/css_value_keywords.h" |
| #include "third_party/blink/renderer/core/permissions_policy/permissions_policy_parser.h" |
| #include "third_party/blink/renderer/modules/manifest/manifest_uma_util.h" |
| #include "third_party/blink/renderer/modules/navigatorcontentutils/navigator_content_utils.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/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/liburlpattern/parse.h" |
| #include "third_party/liburlpattern/pattern.h" |
| #include "url/url_constants.h" |
| #include "url/url_util.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| static constexpr char kOriginWildcardPrefix[] = "%2A."; |
| // Keep in sync with web_app_origin_association_task.cc. |
| static wtf_size_t kMaxUrlHandlersSize = 10; |
| 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().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'; |
| } |
| |
| absl::optional<mojom::blink::ManifestFileHandler::LaunchType> |
| FileHandlerLaunchTypeFromString(const std::string& launch_type) { |
| if (WTF::EqualIgnoringASCIICase(String(launch_type), "single-client")) |
| return mojom::blink::ManifestFileHandler::LaunchType::kSingleClient; |
| if (WTF::EqualIgnoringASCIICase(String(launch_type), "multiple-clients")) |
| return mojom::blink::ManifestFileHandler::LaunchType::kMultipleClients; |
| return absl::nullopt; |
| } |
| |
| } // 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() {} |
| |
| // 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_->name = ParseName(root_object.get()); |
| manifest_->short_name = ParseShortName(root_object.get()); |
| manifest_->description = ParseDescription(root_object.get()); |
| manifest_->start_url = ParseStartURL(root_object.get()); |
| manifest_->id = ParseId(root_object.get(), manifest_->start_url); |
| manifest_->scope = ParseScope(root_object.get(), manifest_->start_url); |
| manifest_->display = ParseDisplay(root_object.get()); |
| manifest_->display_override = ParseDisplayOverride(root_object.get()); |
| manifest_->orientation = ParseOrientation(root_object.get()); |
| manifest_->icons = ParseIcons(root_object.get()); |
| manifest_->screenshots = ParseScreenshots(root_object.get()); |
| |
| auto share_target = ParseShareTarget(root_object.get()); |
| if (share_target.has_value()) |
| manifest_->share_target = std::move(*share_target); |
| |
| manifest_->file_handlers = ParseFileHandlers(root_object.get()); |
| manifest_->protocol_handlers = ParseProtocolHandlers(root_object.get()); |
| manifest_->url_handlers = ParseUrlHandlers(root_object.get()); |
| manifest_->scope_extensions = ParseScopeExtensions(root_object.get()); |
| manifest_->lock_screen = ParseLockScreen(root_object.get()); |
| manifest_->note_taking = ParseNoteTaking(root_object.get()); |
| manifest_->related_applications = ParseRelatedApplications(root_object.get()); |
| manifest_->prefer_related_applications = |
| ParsePreferRelatedApplications(root_object.get()); |
| |
| absl::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; |
| |
| absl::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; |
| |
| manifest_->gcm_sender_id = ParseGCMSenderID(root_object.get()); |
| manifest_->shortcuts = ParseShortcuts(root_object.get()); |
| |
| manifest_->permissions_policy = |
| ParseIsolatedAppPermissions(root_object.get()); |
| |
| manifest_->launch_handler = ParseLaunchHandler(root_object.get()); |
| |
| if (RuntimeEnabledFeatures::WebAppTranslationsEnabled(execution_context_)) { |
| manifest_->translations = ParseTranslations(root_object.get()); |
| } |
| |
| if (RuntimeEnabledFeatures::WebAppDarkModeEnabled(execution_context_)) { |
| manifest_->user_preferences = ParseUserPreferences(root_object.get()); |
| |
| absl::optional<RGBA32> dark_theme_color = |
| ParseDarkColorOverride(root_object.get(), "theme_colors"); |
| manifest_->has_dark_theme_color = dark_theme_color.has_value(); |
| if (manifest_->has_dark_theme_color) |
| manifest_->dark_theme_color = *dark_theme_color; |
| |
| absl::optional<RGBA32> dark_background_color = |
| ParseDarkColorOverride(root_object.get(), "background_colors"); |
| manifest_->has_dark_background_color = dark_background_color.has_value(); |
| if (manifest_->has_dark_background_color) |
| manifest_->dark_background_color = *dark_background_color; |
| } |
| |
| if (RuntimeEnabledFeatures::WebAppTabStripCustomizationsEnabled( |
| execution_context_) && |
| manifest_->display_override.Contains( |
| mojom::blink::DisplayMode::kTabbed)) { |
| manifest_->tab_strip = ParseTabStrip(root_object.get()); |
| } |
| |
| manifest_->version = ParseVersion(root_object.get()); |
| |
| ManifestUmaUtil::ParseSucceeded(manifest_); |
| |
| 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_; |
| } |
| |
| 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("property '" + key + "' ignored, type " + "boolean expected."); |
| return default_value; |
| } |
| |
| return value; |
| } |
| |
| absl::optional<String> ManifestParser::ParseString(const JSONObject* object, |
| const String& key, |
| Trim trim) { |
| JSONValue* json_value = object->Get(key); |
| if (!json_value) |
| return absl::nullopt; |
| |
| String value; |
| if (!json_value->AsString(&value) || value.IsNull()) { |
| AddErrorInfo("property '" + key + "' ignored, type " + "string expected."); |
| return absl::nullopt; |
| } |
| |
| if (trim) |
| value = value.StripWhiteSpace(); |
| return value; |
| } |
| |
| absl::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("property '" + key + "' of '" + member_name + |
| "' not present."); |
| } |
| |
| return absl::nullopt; |
| } |
| |
| String value; |
| if (!json_value->AsString(&value)) { |
| AddErrorInfo("property '" + key + "' of '" + member_name + |
| "' ignored, type string expected."); |
| return absl::nullopt; |
| } |
| if (trim) |
| value = value.StripWhiteSpace(); |
| |
| if (value == "") { |
| AddErrorInfo("property '" + key + "' of '" + member_name + |
| "' is an empty string."); |
| if (required) |
| return absl::nullopt; |
| } |
| |
| return value; |
| } |
| |
| absl::optional<RGBA32> ManifestParser::ParseColor(const JSONObject* object, |
| const String& key) { |
| absl::optional<String> parsed_color = ParseString(object, key, Trim(true)); |
| if (!parsed_color.has_value()) |
| return absl::nullopt; |
| |
| Color color; |
| if (!CSSParser::ParseColor(color, *parsed_color, true)) { |
| AddErrorInfo("property '" + key + "' ignored, '" + *parsed_color + |
| "' is not a " + "valid color."); |
| return absl::nullopt; |
| } |
| |
| return color.Rgb(); |
| } |
| |
| KURL ManifestParser::ParseURL(const JSONObject* object, |
| const String& key, |
| const KURL& base_url, |
| ParseURLRestrictions origin_restriction, |
| bool ignore_empty_string) { |
| absl::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("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("property '" + key + |
| "' ignored, should be same origin as document."); |
| return KURL(); |
| } |
| return resolved; |
| case ParseURLRestrictions::kWithinScope: |
| if (!URLIsWithinScope(resolved, manifest_->scope)) { |
| AddErrorInfo("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(); |
| return KURL(); |
| } |
| |
| 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(key + " value '" + string_value + |
| "' ignored, unknown value."); |
| } |
| return enum_value; |
| } |
| |
| const JSONArray* list = JSONArray::Cast(value); |
| if (!list) { |
| AddErrorInfo("property '" + key + |
| "' ignored, type string or array of strings expected."); |
| return invalid_value; |
| } |
| |
| for (wtf_size_t i = 0; i < list->size(); ++i) { |
| const JSONValue* item = list->at(i); |
| if (!item->AsString(&string_value)) { |
| AddErrorInfo(key + " value '" + item->ToJSONString() + |
| "' ignored, string expected."); |
| continue; |
| } |
| |
| Enum enum_value = parse_enum(string_value.Utf8()); |
| if (enum_value != invalid_value) |
| return enum_value; |
| |
| AddErrorInfo(key + " value '" + string_value + "' ignored, unknown value."); |
| } |
| |
| return invalid_value; |
| } |
| |
| String ManifestParser::ParseName(const JSONObject* object) { |
| absl::optional<String> name = ParseString(object, "name", Trim(true)); |
| if (name.has_value()) { |
| name = name->RemoveCharacters(IsCrLfOrTabChar); |
| if (name->length() == 0) |
| name = absl::nullopt; |
| } |
| return name.has_value() ? *name : String(); |
| } |
| |
| String ManifestParser::ParseShortName(const JSONObject* object) { |
| absl::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 = absl::nullopt; |
| } |
| return short_name.has_value() ? *short_name : String(); |
| } |
| |
| String ManifestParser::ParseDescription(const JSONObject* object) { |
| absl::optional<String> description = |
| ParseString(object, "description", Trim(true)); |
| return description.has_value() ? *description : String(); |
| } |
| |
| KURL ManifestParser::ParseId(const JSONObject* object, const KURL& start_url) { |
| if (!start_url.IsValid()) { |
| ManifestUmaUtil::ParseIdResult( |
| ManifestUmaUtil::ParseIdResultType::kInvalidStartUrl); |
| return KURL(); |
| } |
| KURL start_url_origin = KURL(SecurityOrigin::Create(start_url)->ToString()); |
| |
| KURL id = ParseURL(object, "id", start_url_origin, |
| ParseURLRestrictions::kSameOriginOnly, |
| /*ignore_empty_string=*/true); |
| if (id.IsValid()) { |
| ManifestUmaUtil::ParseIdResult( |
| ManifestUmaUtil::ParseIdResultType::kSucceed); |
| } else { |
| // If id is not specified, sets to start_url |
| ManifestUmaUtil::ParseIdResult( |
| ManifestUmaUtil::ParseIdResultType::kDefaultToStartUrl); |
| id = start_url; |
| } |
| id.RemoveFragmentIdentifier(); |
| return id; |
| } |
| |
| KURL ManifestParser::ParseStartURL(const JSONObject* object) { |
| return ParseURL(object, "start_url", manifest_url_, |
| ParseURLRestrictions::kSameOriginOnly); |
| } |
| |
| KURL ManifestParser::ParseScope(const JSONObject* object, |
| const KURL& start_url) { |
| KURL scope = ParseURL(object, "scope", manifest_url_, |
| ParseURLRestrictions::kNoRestrictions); |
| |
| // This will change to remove the |document_url_| fallback in the future. |
| // See https://github.com/w3c/manifest/issues/668. |
| const KURL& default_value = start_url.IsEmpty() ? document_url_ : start_url; |
| DCHECK(default_value.IsValid()); |
| |
| if (scope.IsEmpty()) |
| return KURL(default_value.BaseAsString()); |
| |
| if (!URLIsWithinScope(default_value, scope)) { |
| AddErrorInfo( |
| "property 'scope' ignored. Start url should be within scope " |
| "of scope URL."); |
| return KURL(default_value.BaseAsString()); |
| } |
| |
| DCHECK(scope.IsValid()); |
| DCHECK(SecurityOrigin::AreSameOrigin(scope, document_url_)); |
| return scope; |
| } |
| |
| blink::mojom::DisplayMode ManifestParser::ParseDisplay( |
| const JSONObject* object) { |
| absl::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 (wtf_size_t i = 0; i < display_override_list->size(); ++i) { |
| 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. |
| display_override_list->at(i)->AsString(&display_enum_string); |
| display_enum_string = display_enum_string.StripWhiteSpace(); |
| mojom::blink::DisplayMode display_enum = |
| DisplayModeFromString(display_enum_string.Utf8()); |
| |
| if (!RuntimeEnabledFeatures::WebAppWindowControlsOverlayEnabled( |
| execution_context_) && |
| display_enum == mojom::blink::DisplayMode::kWindowControlsOverlay) { |
| display_enum = mojom::blink::DisplayMode::kUndefined; |
| } |
| |
| 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) { |
| absl::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) { |
| absl::optional<String> type = ParseString(icon, "type", Trim(true)); |
| return type.has_value() ? *type : String(""); |
| } |
| |
| Vector<gfx::Size> ManifestParser::ParseIconSizes(const JSONObject* icon) { |
| absl::optional<String> sizes_str = ParseString(icon, "sizes", Trim(false)); |
| if (!sizes_str.has_value()) |
| return Vector<gfx::Size>(); |
| |
| WebVector<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; |
| } |
| |
| absl::optional<Vector<mojom::blink::ManifestImageResource::Purpose>> |
| ManifestParser::ParseIconPurpose(const JSONObject* icon) { |
| absl::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 absl::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) { |
| absl::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) { |
| absl::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); |
| } |
| |
| 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 (wtf_size_t i = 0; i < screenshots_list->size(); ++i) { |
| JSONObject* screenshot_object = JSONObject::Cast(screenshots_list->at(i)); |
| 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("property '" + key + "' ignored, type array expected."); |
| return icons; |
| } |
| |
| for (wtf_size_t i = 0; i < icons_list->size(); ++i) { |
| auto icon = ParseImageResource(icons_list->at(i)); |
| if (icon.has_value()) |
| icons.push_back(std::move(*icon)); |
| } |
| |
| return icons; |
| } |
| |
| absl::optional<mojom::blink::ManifestImageResourcePtr> |
| ManifestParser::ParseImageResource(const JSONValue* object) { |
| const JSONObject* icon_object = JSONObject::Cast(object); |
| if (!icon_object) |
| return absl::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 absl::nullopt; |
| |
| icon->type = ParseIconType(icon_object); |
| icon->sizes = ParseIconSizes(icon_object); |
| auto purpose = ParseIconPurpose(icon_object); |
| if (!purpose) |
| return absl::nullopt; |
| |
| icon->purpose = std::move(*purpose); |
| return icon; |
| } |
| |
| String ManifestParser::ParseShortcutName(const JSONObject* shortcut) { |
| absl::optional<String> name = |
| ParseStringForMember(shortcut, "shortcut", "name", true, Trim(true)); |
| return name.has_value() ? *name : String(); |
| } |
| |
| String ManifestParser::ParseShortcutShortName(const JSONObject* shortcut) { |
| absl::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) { |
| absl::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("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); |
| auto icons = ParseIcons(shortcut_object); |
| if (!icons.empty()) |
| shortcut->icons = std::move(icons); |
| |
| shortcuts.push_back(std::move(shortcut)); |
| } |
| |
| return shortcuts; |
| } |
| |
| String ManifestParser::ParseFileFilterName(const JSONObject* file) { |
| if (!file->Get("name")) { |
| AddErrorInfo("property 'name' missing."); |
| return String(""); |
| } |
| |
| String value; |
| if (!file->GetString("name", &value)) { |
| AddErrorInfo("property 'name' ignored, type string expected."); |
| return 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 (wtf_size_t i = 0; i < accept_list->size(); ++i) { |
| JSONValue* accept_value = accept_list->at(i); |
| String accept_string; |
| if (!accept_value || !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 (wtf_size_t i = 0; i < file_list->size(); ++i) { |
| const JSONObject* file_object = JSONObject::Cast(file_list->at(i)); |
| 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)); |
| } |
| |
| absl::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 absl::nullopt; |
| |
| String method = value.UpperASCII(); |
| if (method == "GET") |
| return mojom::blink::ManifestShareTarget::Method::kGet; |
| if (method == "POST") |
| return mojom::blink::ManifestShareTarget::Method::kPost; |
| |
| return absl::nullopt; |
| } |
| |
| absl::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 absl::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 absl::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. |
| absl::optional<String> text = |
| ParseString(share_target_params, "text", Trim(true)); |
| params->text = text.has_value() ? *text : String(); |
| absl::optional<String> title = |
| ParseString(share_target_params, "title", Trim(true)); |
| params->title = title.has_value() ? *title : String(); |
| absl::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; |
| } |
| |
| absl::optional<mojom::blink::ManifestShareTargetPtr> |
| ManifestParser::ParseShareTarget(const JSONObject* object) { |
| const JSONObject* share_target_object = object->GetJSONObject("share_target"); |
| if (!share_target_object) |
| return absl::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 absl::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 absl::nullopt; |
| } |
| |
| share_target->params = ParseShareTargetParams(share_target_params_object); |
| if (!method.has_value()) { |
| AddErrorInfo( |
| "invalid method. Allowed methods are:" |
| "GET and POST."); |
| return absl::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 absl::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 absl::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 absl::nullopt; |
| } |
| } |
| |
| if (share_target->params->files.has_value() && |
| !VerifyFiles(*share_target->params->files)) { |
| AddErrorInfo("invalid mime type inside files."); |
| return absl::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 (wtf_size_t i = 0; i < entry_array->size(); ++i) { |
| JSONObject* json_entry = JSONObject::Cast(entry_array->at(i)); |
| if (!json_entry) { |
| AddErrorInfo("FileHandler ignored, type object expected."); |
| continue; |
| } |
| |
| absl::optional<mojom::blink::ManifestFileHandlerPtr> entry = |
| ParseFileHandler(json_entry); |
| if (!entry) |
| continue; |
| |
| result.push_back(std::move(entry.value())); |
| } |
| |
| return result; |
| } |
| |
| absl::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 absl::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 absl::nullopt; |
| } |
| |
| entry->launch_type = |
| ParseFirstValidEnum< |
| absl::optional<mojom::blink::ManifestFileHandler::LaunchType>>( |
| file_handler, "launch_type", &FileHandlerLaunchTypeFromString, |
| /*invalid_value=*/absl::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("invalid MIME type: " + mimetype); |
| continue; |
| } |
| |
| Vector<String> extensions; |
| String extension; |
| JSONArray* extensions_array = JSONArray::Cast(entry.second); |
| if (extensions_array) { |
| for (wtf_size_t j = 0; j < extensions_array->size(); ++j) { |
| JSONValue* value = extensions_array->at(j); |
| 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 = extensions.end() - extension_overflow; |
| AddErrorInfo( |
| "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 (wtf_size_t i = 0; i < protocol_list->size(); ++i) { |
| const JSONObject* protocol_object = JSONObject::Cast(protocol_list->at(i)); |
| if (!protocol_object) { |
| AddErrorInfo("protocol_handlers entry ignored, type object expected."); |
| continue; |
| } |
| |
| absl::optional<mojom::blink::ManifestProtocolHandlerPtr> protocol = |
| ParseProtocolHandler(protocol_object); |
| if (!protocol) |
| continue; |
| |
| protocols.push_back(std::move(protocol.value())); |
| } |
| |
| return protocols; |
| } |
| |
| absl::optional<mojom::blink::ManifestProtocolHandlerPtr> |
| ManifestParser::ParseProtocolHandler(const JSONObject* object) { |
| if (!object->Get("protocol")) { |
| AddErrorInfo( |
| "protocol_handlers entry ignored, required property 'protocol' is " |
| "missing."); |
| return absl::nullopt; |
| } |
| |
| auto protocol_handler = mojom::blink::ManifestProtocolHandler::New(); |
| absl::optional<String> protocol = ParseString(object, "protocol", Trim(true)); |
| String error_message; |
| bool is_valid_protocol = protocol.has_value(); |
| |
| if (is_valid_protocol && |
| !VerifyCustomHandlerScheme(protocol.value(), error_message, |
| ProtocolHandlerSecurityLevel::kStrict)) { |
| AddErrorInfo(error_message); |
| is_valid_protocol = false; |
| } |
| |
| if (!is_valid_protocol) { |
| AddErrorInfo( |
| "protocol_handlers entry ignored, required property 'protocol' is " |
| "invalid."); |
| return absl::nullopt; |
| } |
| protocol_handler->protocol = protocol.value(); |
| |
| if (!object->Get("url")) { |
| AddErrorInfo( |
| "protocol_handlers entry ignored, required property 'url' is missing."); |
| return absl::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, |
| error_message)) { |
| AddErrorInfo(error_message); |
| is_valid_url = false; |
| } |
| } |
| |
| if (!is_valid_url) { |
| AddErrorInfo( |
| "protocol_handlers entry ignored, required property 'url' is invalid."); |
| return absl::nullopt; |
| } |
| |
| return std::move(protocol_handler); |
| } |
| |
| Vector<mojom::blink::ManifestUrlHandlerPtr> ManifestParser::ParseUrlHandlers( |
| const JSONObject* from) { |
| Vector<mojom::blink::ManifestUrlHandlerPtr> url_handlers; |
| const bool feature_enabled = |
| base::FeatureList::IsEnabled(blink::features::kWebAppEnableUrlHandlers) || |
| RuntimeEnabledFeatures::WebAppUrlHandlingEnabled(execution_context_); |
| if (!feature_enabled || !from->Get("url_handlers")) { |
| return url_handlers; |
| } |
| JSONArray* handlers_list = from->GetArray("url_handlers"); |
| if (!handlers_list) { |
| AddErrorInfo("property 'url_handlers' ignored, type array expected."); |
| return url_handlers; |
| } |
| for (wtf_size_t i = 0; i < handlers_list->size(); ++i) { |
| if (i == kMaxUrlHandlersSize) { |
| AddErrorInfo("property 'url_handlers' contains more than " + |
| String::Number(kMaxUrlHandlersSize) + |
| " valid elements, only the first " + |
| String::Number(kMaxUrlHandlersSize) + " are parsed."); |
| break; |
| } |
| |
| const JSONObject* handler_object = JSONObject::Cast(handlers_list->at(i)); |
| if (!handler_object) { |
| AddErrorInfo("url_handlers entry ignored, type object expected."); |
| continue; |
| } |
| |
| absl::optional<mojom::blink::ManifestUrlHandlerPtr> url_handler = |
| ParseUrlHandler(handler_object); |
| if (!url_handler) { |
| continue; |
| } |
| url_handlers.push_back(std::move(url_handler.value())); |
| } |
| return url_handlers; |
| } |
| |
| absl::optional<mojom::blink::ManifestUrlHandlerPtr> |
| ManifestParser::ParseUrlHandler(const JSONObject* object) { |
| DCHECK( |
| base::FeatureList::IsEnabled(blink::features::kWebAppEnableUrlHandlers) || |
| RuntimeEnabledFeatures::WebAppUrlHandlingEnabled(execution_context_)); |
| if (!object->Get("origin")) { |
| AddErrorInfo( |
| "url_handlers entry ignored, required property 'origin' is missing."); |
| return absl::nullopt; |
| } |
| const absl::optional<String> origin_string = |
| ParseString(object, "origin", Trim(true)); |
| if (!origin_string.has_value()) { |
| AddErrorInfo( |
| "url_handlers entry ignored, required property 'origin' is invalid."); |
| return absl::nullopt; |
| } |
| |
| // TODO(crbug.com/1072058): pre-process for input without scheme. |
| // (eg. example.com instead of https://example.com) because we can always |
| // assume the use of https for URL handling. Remove this TODO if we decide |
| // to require fully specified https scheme in this origin input. |
| |
| if (origin_string->length() > kMaxOriginLength) { |
| AddErrorInfo( |
| "url_handlers entry ignored, 'origin' exceeds maximum character length " |
| "of " + |
| String::Number(kMaxOriginLength) + " ."); |
| return absl::nullopt; |
| } |
| |
| auto origin = SecurityOrigin::CreateFromString(*origin_string); |
| if (!origin || origin->IsOpaque()) { |
| AddErrorInfo( |
| "url_handlers entry ignored, required property 'origin' is invalid."); |
| return absl::nullopt; |
| } |
| if (origin->Protocol() != url::kHttpsScheme) { |
| AddErrorInfo( |
| "url_handlers entry ignored, required property 'origin' must use the " |
| "https scheme."); |
| return absl::nullopt; |
| } |
| |
| String host = origin->Host(); |
| auto url_handler = mojom::blink::ManifestUrlHandler::New(); |
| // Check for wildcard *. |
| if (host.StartsWith(kOriginWildcardPrefix)) { |
| url_handler->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 { |
| url_handler->has_origin_wildcard = false; |
| } |
| |
| bool host_valid = IsHostValidForScopeExtension(host); |
| if (!host_valid) { |
| AddErrorInfo( |
| "url_handlers entry ignored, domain of required property 'origin' is " |
| "invalid."); |
| return absl::nullopt; |
| } |
| |
| if (url_handler->has_origin_wildcard) { |
| origin = SecurityOrigin::CreateFromValidTuple(origin->Protocol(), host, |
| origin->Port()); |
| if (!origin_string.has_value()) { |
| AddErrorInfo( |
| "url_handlers entry ignored, required property 'origin' is invalid."); |
| return absl::nullopt; |
| } |
| } |
| |
| url_handler->origin = origin; |
| return std::move(url_handler); |
| } |
| |
| Vector<mojom::blink::ManifestScopeExtensionPtr> |
| ManifestParser::ParseScopeExtensions(const JSONObject* from) { |
| Vector<mojom::blink::ManifestScopeExtensionPtr> scope_extensions; |
| if (!base::FeatureList::IsEnabled( |
| blink::features::kWebAppEnableScopeExtensions) || |
| !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; |
| } |
| |
| JSONValue::ValueType expected_entry_type = JSONValue::kTypeNull; |
| for (wtf_size_t i = 0; i < extensions_list->size(); ++i) { |
| if (i == kMaxScopeExtensionsSize) { |
| AddErrorInfo("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::kTypeString && |
| entry_type != JSONValue::kTypeObject) { |
| AddErrorInfo( |
| "scope_extensions entry ignored, type string or object expected."); |
| continue; |
| } |
| |
| // Check whether first scope extension entry in the list is a string or |
| // object to make sure that following entries have the same type, ignoring |
| // entries that are null or other types. |
| if (expected_entry_type != JSONValue::kTypeString && |
| expected_entry_type != JSONValue::kTypeObject) { |
| expected_entry_type = entry_type; |
| } |
| |
| absl::optional<mojom::blink::ManifestScopeExtensionPtr> scope_extension = |
| absl::nullopt; |
| if (expected_entry_type == JSONValue::kTypeString) { |
| String scope_extension_origin; |
| if (!extensions_entry->AsString(&scope_extension_origin)) { |
| AddErrorInfo("scope_extensions entry ignored, type string expected."); |
| continue; |
| } |
| scope_extension = ParseScopeExtensionOrigin(scope_extension_origin); |
| } else { |
| 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; |
| } |
| |
| absl::optional<mojom::blink::ManifestScopeExtensionPtr> |
| ManifestParser::ParseScopeExtension(const JSONObject* object) { |
| DCHECK(base::FeatureList::IsEnabled( |
| blink::features::kWebAppEnableScopeExtensions)); |
| if (!object->Get("origin")) { |
| AddErrorInfo( |
| "scope_extensions entry ignored, required property 'origin' is " |
| "missing."); |
| return absl::nullopt; |
| } |
| const absl::optional<String> origin_string = |
| ParseString(object, "origin", Trim(true)); |
| if (!origin_string.has_value()) { |
| return absl::nullopt; |
| } |
| |
| return ParseScopeExtensionOrigin(*origin_string); |
| } |
| |
| absl::optional<mojom::blink::ManifestScopeExtensionPtr> |
| ManifestParser::ParseScopeExtensionOrigin(const String& origin_string) { |
| DCHECK(base::FeatureList::IsEnabled( |
| blink::features::kWebAppEnableScopeExtensions)); |
| |
| // 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( |
| "scope_extensions entry ignored, 'origin' exceeds maximum character " |
| "length of " + |
| String::Number(kMaxOriginLength) + " ."); |
| return absl::nullopt; |
| } |
| |
| auto origin = SecurityOrigin::CreateFromString(origin_string); |
| if (!origin || origin->IsOpaque()) { |
| AddErrorInfo( |
| "scope_extensions entry ignored, required property 'origin' is " |
| "invalid."); |
| return absl::nullopt; |
| } |
| if (origin->Protocol() != url::kHttpsScheme) { |
| AddErrorInfo( |
| "scope_extensions entry ignored, required property 'origin' must use " |
| "the https scheme."); |
| return absl::nullopt; |
| } |
| |
| String host = origin->Host(); |
| auto scope_extension = mojom::blink::ManifestScopeExtension::New(); |
| // Check for wildcard *. |
| if (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 absl::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 absl::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) { |
| absl::optional<String> platform = |
| ParseString(application, "platform", Trim(true)); |
| return platform.has_value() ? *platform : String(); |
| } |
| |
| absl::optional<KURL> ManifestParser::ParseRelatedApplicationURL( |
| const JSONObject* application) { |
| return ParseURL(application, "url", manifest_url_, |
| ParseURLRestrictions::kNoRestrictions); |
| } |
| |
| String ManifestParser::ParseRelatedApplicationId( |
| const JSONObject* application) { |
| absl::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 (wtf_size_t i = 0; i < applications_list->size(); ++i) { |
| const JSONObject* application_object = |
| JSONObject::Cast(applications_list->at(i)); |
| 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); |
| } |
| |
| absl::optional<RGBA32> ManifestParser::ParseThemeColor( |
| const JSONObject* object) { |
| return ParseColor(object, "theme_color"); |
| } |
| |
| absl::optional<RGBA32> ManifestParser::ParseBackgroundColor( |
| const JSONObject* object) { |
| return ParseColor(object, "background_color"); |
| } |
| |
| String ManifestParser::ParseGCMSenderID(const JSONObject* object) { |
| absl::optional<String> gcm_sender_id = |
| ParseString(object, "gcm_sender_id", Trim(true)); |
| return gcm_sender_id.has_value() ? *gcm_sender_id : String(); |
| } |
| |
| Vector<blink::ParsedPermissionsPolicyDeclaration> |
| ManifestParser::ParseIsolatedAppPermissions(const JSONObject* object) { |
| PermissionsPolicyParser::Node policy{ |
| OriginWithPossibleWildcards::NodeType::kHeader}; |
| |
| JSONValue* json_value = object->Get("permissions_policy"); |
| if (!json_value) |
| return Vector<blink::ParsedPermissionsPolicyDeclaration>(); |
| |
| JSONObject* permissions_dict = object->GetJSONObject("permissions_policy"); |
| if (!permissions_dict) { |
| AddErrorInfo( |
| "property 'permissions_policy' ignored, type object expected."); |
| return Vector<blink::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("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 origin strings to be wrapped in single |
| // quotes, as they would be in the header's permissions policy string. The |
| // asterisk is a token, which does not need to be wrapped in single |
| // quotes. |
| String wrapped_origin = (origin == "*" ? origin : "'" + origin + "'"); |
| new_policy.allowlist.push_back(wrapped_origin); |
| } |
| policy.declarations.push_back(new_policy); |
| } |
| |
| PolicyParserMessageBuffer logger( |
| "Error with permissions_policy manifest field: "); |
| blink::ParsedPermissionsPolicy parsed_policy = |
| PermissionsPolicyParser::ParsePolicyFromNode( |
| policy, SecurityOrigin::Create(manifest_url_), logger, |
| execution_context_); |
| |
| Vector<blink::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 (wtf_size_t i = 0; i < json_allowlist->size(); ++i) { |
| JSONValue* json_value = json_allowlist->at(i); |
| if (!json_value) { |
| AddErrorInfo( |
| "permissions_policy entry ignored, required property 'origin' is " |
| "invalid."); |
| return Vector<String>(); |
| } |
| |
| 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( |
| "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) { |
| if (!RuntimeEnabledFeatures::WebAppLaunchHandlerEnabled(execution_context_)) |
| return nullptr; |
| |
| 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<absl::optional<ClientMode>>( |
| launch_handler_object, "client_mode", &ClientModeFromString, |
| /*invalid_value=*/absl::nullopt) |
| .value_or(ClientMode::kAuto)); |
| } |
| |
| 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(); |
| |
| absl::optional<String> name = ParseStringForMember( |
| translation, "translations", "name", false, Trim(true)); |
| translation_item->name = |
| name.has_value() && name->length() != 0 ? *name : String(); |
| |
| absl::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(); |
| |
| absl::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::ManifestUserPreferenceOverridesPtr |
| ManifestParser::ParsePreferenceOverrides(const JSONObject* object, |
| const String& preference) { |
| auto user_preference_overrides = |
| mojom::blink::ManifestUserPreferenceOverrides::New(); |
| |
| if (!object->Get(preference)) |
| return nullptr; |
| |
| JSONObject* overrides = object->GetJSONObject(preference); |
| if (!overrides) { |
| AddErrorInfo("preference '" + preference + "' ignored, object expected."); |
| return nullptr; |
| } |
| |
| absl::optional<RGBA32> theme_color = ParseThemeColor(overrides); |
| user_preference_overrides->has_theme_color = theme_color.has_value(); |
| if (user_preference_overrides->has_theme_color) |
| user_preference_overrides->theme_color = *theme_color; |
| |
| absl::optional<RGBA32> background_color = ParseBackgroundColor(overrides); |
| user_preference_overrides->has_background_color = |
| background_color.has_value(); |
| if (user_preference_overrides->has_background_color) |
| user_preference_overrides->background_color = *background_color; |
| |
| // All of the fields that can be overridden by user_preferences are |
| // optional. If no overrides are supplied, skip the preference. |
| if (!user_preference_overrides->has_theme_color && |
| !user_preference_overrides->has_background_color) { |
| return nullptr; |
| } |
| return user_preference_overrides; |
| } |
| |
| mojom::blink::ManifestUserPreferencesPtr ManifestParser::ParseUserPreferences( |
| const JSONObject* object) { |
| auto result = mojom::blink::ManifestUserPreferences::New(); |
| |
| if (!object->Get("user_preferences")) |
| return nullptr; |
| |
| JSONObject* user_preferences_map = object->GetJSONObject("user_preferences"); |
| if (!user_preferences_map) { |
| AddErrorInfo("property 'user_preferences' ignored, object expected."); |
| return nullptr; |
| } |
| |
| if (user_preferences_map->Get("color_scheme")) { |
| JSONObject* color_scheme_map = |
| user_preferences_map->GetJSONObject("color_scheme"); |
| if (!color_scheme_map) { |
| AddErrorInfo("property 'color_scheme' ignored, object expected."); |
| return nullptr; |
| } |
| result->color_scheme_dark = |
| ParsePreferenceOverrides(color_scheme_map, "dark"); |
| } else { |
| // TODO(crbug.com/1318305): Remove this path once the new format has become |
| // the norm. |
| result->color_scheme_dark = |
| ParsePreferenceOverrides(user_preferences_map, "color_scheme_dark"); |
| } |
| |
| return result; |
| } |
| |
| absl::optional<RGBA32> ManifestParser::ParseDarkColorOverride( |
| const JSONObject* object, |
| const String& key) { |
| JSONValue* json_value = object->Get(key); |
| if (!json_value) |
| return absl::nullopt; |
| |
| JSONArray* colors_list = object->GetArray(key); |
| if (!colors_list) { |
| AddErrorInfo("property '" + key + "' ignored, type array expected."); |
| return absl::nullopt; |
| } |
| |
| MediaValuesCached::MediaValuesCachedData media_values_data; |
| media_values_data.preferred_color_scheme = |
| mojom::blink::PreferredColorScheme::kDark; |
| |
| MediaQueryEvaluator media_query_evaluator( |
| MakeGarbageCollected<MediaValuesCached>(media_values_data)); |
| |
| for (wtf_size_t i = 0; i < colors_list->size(); ++i) { |
| const JSONObject* list_item = JSONObject::Cast(colors_list->at(i)); |
| if (!list_item) |
| continue; |
| |
| absl::optional<String> media_query = |
| ParseString(list_item, "media", Trim(false)); |
| absl::optional<RGBA32> color = ParseColor(list_item, "color"); |
| if (!media_query.has_value() || !color.has_value()) |
| continue; |
| |
| auto tokens = CSSTokenizer(media_query.value()).TokenizeToEOF(); |
| CSSParserTokenRange range(tokens); |
| while (!range.AtEnd()) { |
| if (range.Peek().GetType() == kIdentToken && |
| (range.Peek().Value().ToString().LowerASCII() != |
| "prefers-color-scheme" && |
| range.Peek().Id() != CSSValueID::kDark)) { |
| // Skip the query if it contains anything other than |
| // "(prefers-color-scheme: dark)". |
| break; |
| } |
| range.Consume(); |
| if (range.AtEnd() && media_query_evaluator.Eval(*MediaQuerySet::Create( |
| media_query.value(), execution_context_))) { |
| return color.value(); |
| } |
| } |
| } |
| |
| return absl::nullopt; |
| } |
| |
| 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) && |
| string_value.LowerASCII() == "auto")) { |
| home_tab_params->icons = ParseIcons(home_tab_object); |
| } |
| |
| home_tab_params->scope_patterns = ParseScopePatterns(home_tab_object); |
| |
| result->home_tab = |
| mojom::blink::HomeTabUnion::NewParams(std::move(home_tab_params)); |
| } else { |
| result->home_tab = mojom::blink::HomeTabUnion::NewVisibility( |
| ParseTabStripMemberVisibility(home_tab_value)); |
| } |
| |
| JSONValue* new_tab_button_value = tab_strip_object->Get("new_tab_button"); |
| if (new_tab_button_value && |
| new_tab_button_value->GetType() == JSONValue::kTypeObject) { |
| JSONObject* new_tab_button_object = |
| tab_strip_object->GetJSONObject("new_tab_button"); |
| JSONValue* new_tab_button_url = new_tab_button_object->Get("url"); |
| |
| auto new_tab_button_params = mojom::blink::NewTabButtonParams::New(); |
| String string_value; |
| if (new_tab_button_url && !(new_tab_button_url->AsString(&string_value) && |
| string_value.LowerASCII() == "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 = mojom::blink::NewTabButtonUnion::NewParams( |
| std::move(new_tab_button_params)); |
| } else { |
| result->new_tab_button = mojom::blink::NewTabButtonUnion::NewVisibility( |
| ParseTabStripMemberVisibility(new_tab_button_value)); |
| } |
| |
| 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) && |
| string_value.LowerASCII() == "absent") { |
| return mojom::blink::TabStripMemberVisibility::kAbsent; |
| } |
| |
| return mojom::blink::TabStripMemberVisibility::kAuto; |
| } |
| |
| Vector<UrlPattern> ManifestParser::ParseScopePatterns( |
| const JSONObject* object) { |
| Vector<UrlPattern> result; |
| |
| if (!object->Get("scope_patterns")) { |
| return result; |
| } |
| |
| JSONArray* scope_patterns_list = object->GetArray("scope_patterns"); |
| if (!scope_patterns_list) { |
| return result; |
| } |
| |
| for (wtf_size_t i = 0; i < scope_patterns_list->size(); ++i) { |
| UrlPattern url_pattern; |
| |
| JSONObject* pattern_object = JSONObject::Cast(scope_patterns_list->at(i)); |
| if (!pattern_object) { |
| continue; |
| } |
| |
| absl::optional<String> pathname = ParseStringForMember( |
| pattern_object, "scope_patterns", "pathname", false, Trim(true)); |
| if (pathname.has_value()) { |
| StringUTF8Adaptor utf8(pathname.value()); |
| auto parse_result = liburlpattern::Parse( |
| absl::string_view(utf8.data(), utf8.size()), |
| [](absl::string_view input) { return std::string(input); }); |
| |
| if (parse_result.ok()) { |
| std::vector<liburlpattern::Part> part_list; |
| bool is_valid_pattern = true; |
| 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) { |
| is_valid_pattern = false; |
| break; |
| } |
| |
| part_list.push_back(std::move(part)); |
| } |
| if (is_valid_pattern) { |
| url_pattern.pathname = std::move(part_list); |
| result.push_back(std::move(url_pattern)); |
| } |
| } |
| } |
| } |
| |
| return result; |
| } |
| |
| 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)); |
| } |
| |
| } // namespace blink |