blob: 8f996963a04f75e132931eb5026b8f5e2d0fadec [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/common/manifest_handlers/web_file_handlers_info.h"
#include <string_view>
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/utf_string_conversions.h"
#include "extensions/common/api/file_handlers.h"
#include "extensions/common/error_utils.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/features/feature.h"
#include "extensions/common/features/feature_provider.h"
#include "extensions/common/install_warning.h"
#include "extensions/common/manifest.h"
#include "extensions/common/manifest_constants.h"
namespace extensions {
namespace {
using FileHandlersManifestKeys = api::file_handlers::ManifestKeys;
bool IsInAllowlist(const Extension& extension) {
const Feature* feature = FeatureProvider::GetManifestFeature("file_handlers");
return feature->IsIdInAllowlist(extension.hashed_id());
}
// Verifies manifest input. Disambiguates `file_extensions` on `accept` into a
// list, which could otherwise have also been a string. `icon.sizes` remains as
// is because the generated data type only accepts a string. This string can be
// parsed with a method that gets a list of sizes.
// TODO(crbug.com/40169582): Re-use Blink parser.
std::unique_ptr<WebFileHandlers> ParseFromList(const Extension& extension,
std::u16string* error) {
FileHandlersManifestKeys manifest_keys;
if (!FileHandlersManifestKeys::ParseFromDictionary(
extension.manifest()->available_values(), manifest_keys, *error)) {
return nullptr;
}
auto get_error = [](size_t i, std::string_view message) {
return ErrorUtils::FormatErrorMessageUTF16(
manifest_errors::kInvalidWebFileHandlers, base::NumberToString(i),
message);
};
auto info = std::make_unique<WebFileHandlers>();
// file_handlers: array. can't be empty
if (manifest_keys.file_handlers.empty()) {
*error = get_error(0, "At least one File Handler must be present.");
return nullptr;
}
for (size_t i = 0; i < manifest_keys.file_handlers.size(); i++) {
WebFileHandler web_file_handler;
auto& manifest_file_handler = manifest_keys.file_handlers[i];
// `name` is a string that can't be empty.
if (manifest_file_handler.name.empty()) {
*error = get_error(i, "`name` must have a value.");
return nullptr;
}
web_file_handler.file_handler.name = std::move(manifest_file_handler.name);
// `action` is a string that can't be empty and starts with slash.
if (manifest_file_handler.action.empty()) {
*error = get_error(i, "`action` must have a value.");
return nullptr;
} else if (manifest_file_handler.action[0] != '/') {
*error = get_error(i, "`action` must start with a forward slash.");
return nullptr;
}
web_file_handler.file_handler.action =
std::move(manifest_file_handler.action);
// `accept` is a dictionary. MIME types are strings with one slash. File
// extensions are strings or an array of strings where each string has a
// leading period.
if (manifest_file_handler.accept.additional_properties.empty()) {
*error = get_error(i, "`accept` cannot be empty.");
return nullptr;
}
// Mime type keyed by string or array of strings of file extensions.
base::Value::Dict accept;
for (const auto [mime_type, file_extensions] :
manifest_file_handler.accept.additional_properties) {
// Verify that mime type only has one slash.
// TODO(crbug.com/40169582): Verify that slash isn't the first or last
// char.
// TODO(crbug.com/40169582): Cross-check slash against canonical mime
// list.
auto num_slashes = std::count(mime_type.begin(), mime_type.end(), '/');
if (num_slashes != 1) {
*error =
get_error(i, "`accept` mime type must have exactly one slash.");
return nullptr;
}
// Verify that file extension has a leading dot.
base::Value::List file_extension_list;
if (file_extensions.is_string()) {
file_extension_list.Append(file_extensions.GetString());
} else if (file_extensions.is_list()) {
file_extension_list = file_extensions.GetList().Clone();
} else {
*error = get_error(i, "`accept` must have a valid file extension.");
return nullptr;
}
if (file_extension_list.empty()) {
*error = get_error(i, "`accept` file extension must have a value.");
return nullptr;
}
// Verify file extensions in `accept`.
for (const auto& file_extension : file_extension_list) {
auto file_extension_item = file_extension.GetString();
if (file_extension_item.empty()) {
*error = get_error(i, "`accept` file extension must have a value.");
return nullptr;
}
if (file_extension_item[0] != '.') {
*error = get_error(
i, "`accept` file extension must have a leading period.");
return nullptr;
}
}
// TODO(crbug.com/40169582): Error if there are duplicate mime_types.
accept.Set(mime_type, std::move(file_extension_list));
}
// Make the temporary `accept` permanent by assigning to `file_handler`.
if (auto result =
api::file_handlers::FileHandler::Accept::FromValue(accept);
result.has_value()) {
web_file_handler.file_handler.accept = std::move(result).value();
} else {
*error = result.error();
}
// `icon` is an optional array of dictionaries.
if (manifest_file_handler.icons.has_value()) {
for (const auto& icon : manifest_file_handler.icons.value()) {
if (icon.src.empty()) {
*error = get_error(i, "`icon.src` must have a value.");
return nullptr;
}
if (icon.sizes.has_value()) {
auto& sizes = icon.sizes.value();
if (sizes.empty()) {
*error = get_error(i, "`icon.sizes` must have a value.");
return nullptr;
}
auto size_list =
base::SplitString(sizes, " ", base::TRIM_WHITESPACE,
base::SplitResult::SPLIT_WANT_NONEMPTY);
for (const auto& size : size_list) {
auto dimensions =
base::SplitString(size, "x", base::TRIM_WHITESPACE,
base::SplitResult::SPLIT_WANT_NONEMPTY);
if (dimensions.size() != 2) {
*error = get_error(i, "`icon.sizes` must have width and height.");
return nullptr;
}
for (const auto& dimension : dimensions) {
int parsed_dimension = 0;
if (!base::StringToInt(dimension, &parsed_dimension)) {
*error =
get_error(i, "`icon.sizes` dimensions must be digits.");
return nullptr;
}
}
}
}
}
// Append icon.
web_file_handler.file_handler.icons =
std::move(manifest_file_handler.icons);
}
// `launch_type` is an optional string that defaults to "single-client".
{
web_file_handler.file_handler.launch_type =
std::move(manifest_file_handler.launch_type);
const std::string launch_type =
web_file_handler.file_handler.launch_type.value_or("single-client");
// Use an enum for potential validity enforcement and typed comparison.
if (launch_type == "single-client") {
web_file_handler.launch_type =
WebFileHandler::LaunchType::kSingleClient;
} else if (launch_type == "multiple-clients") {
web_file_handler.launch_type =
WebFileHandler::LaunchType::kMultipleClients;
} else {
*error = get_error(i, "`launch_type` must have a valid value.");
return nullptr;
}
}
// Append file handlers.
info->file_handlers.emplace_back(std::move(web_file_handler));
}
return info;
}
} // namespace
WebFileHandlers::WebFileHandlers() = default;
WebFileHandlers::~WebFileHandlers() = default;
// static
bool WebFileHandlers::HasFileHandlers(const Extension& extension) {
const WebFileHandlersInfo* info = GetFileHandlers(extension);
return info && info->size() > 0;
}
// static
const WebFileHandlersInfo* WebFileHandlers::GetFileHandlers(
const Extension& extension) {
// Guard against incompatible extension manifest versions.
if (!WebFileHandlers::SupportsWebFileHandlers(extension)) {
return nullptr;
}
WebFileHandlers* info = static_cast<WebFileHandlers*>(
extension.GetManifestData(manifest_keys::kFileHandlers));
return info ? &info->file_handlers : nullptr;
}
WebFileHandlersParser::WebFileHandlersParser() = default;
WebFileHandlersParser::~WebFileHandlersParser() = default;
bool WebFileHandlersParser::Parse(Extension* extension, std::u16string* error) {
CHECK(extension);
// Only parse if Web File Handlers supported in this session. If they are not,
// the install will succeed with a warning, and the key won't be parsed.
// TODO(crbug.com/40268398): Remove this after launching web file handlers.
if (!WebFileHandlers::SupportsWebFileHandlers(*extension)) {
extension->AddInstallWarning(InstallWarning(ErrorUtils::FormatErrorMessage(
manifest_errors::kUnrecognizedManifestKey, "file_handlers")));
return true;
}
// Parse the manifest key as a Web File Handler.
auto info = ParseFromList(*extension, error);
if (!info) {
return false;
}
extension->SetManifestData(manifest_keys::kFileHandlers, std::move(info));
return true;
}
base::span<const char* const> WebFileHandlersParser::Keys() const {
static constexpr const char* kKeys[] = {
FileHandlersManifestKeys::kFileHandlers};
return kKeys;
}
bool WebFileHandlersParser::Validate(
const Extension& extension,
std::string* error,
std::vector<InstallWarning>* warnings) const {
// TODO(crbug.com/40832486): Verify that icons exist.
return true;
}
// static
bool WebFileHandlers::SupportsWebFileHandlers(const Extension& extension) {
return extension.manifest_version() >= 3 && extension.is_extension();
}
// static
bool WebFileHandlers::CanBypassPermissionDialog(const Extension& extension) {
return IsInAllowlist(extension) || extension.was_installed_by_default();
}
} // namespace extensions