blob: 87b89a0357400255f29265e89394ae8558ee5c95 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/extensions/convert_web_app.h"
#include <stddef.h>
#include <stdint.h>
#include <limits>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/base64.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/json/json_file_value_serializer.h"
#include "base/logging.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string16.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/web_applications/components/web_app_helpers.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/extensions/api/url_handlers/url_handlers_parser.h"
#include "chrome/common/extensions/manifest_handlers/app_theme_color_info.h"
#include "chrome/common/web_application_info.h"
#include "content/public/common/url_constants.h"
#include "crypto/sha2.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/file_util.h"
#include "extensions/common/image_util.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/manifest_handlers/file_handler_info.h"
#include "net/base/url_util.h"
#include "third_party/blink/public/common/manifest/manifest_util.h"
#include "third_party/blink/public/mojom/manifest/display_mode.mojom.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/safe_integer_conversions.h"
#include "url/gurl.h"
namespace extensions {
namespace keys = manifest_keys;
namespace {
const char kIconsDirName[] = "icons";
const char kScopeUrlHandlerId[] = "scope";
const char kShortcutIconsDirName[] = "shortcut_icons";
bool IsValidFileExtension(const std::string& file_extension) {
return !file_extension.empty() && file_extension[0] == '.';
}
base::Value CreateFileHandlersForBookmarkApp(
const std::vector<blink::Manifest::FileHandler>& manifest_file_handlers) {
base::Value file_handlers(base::Value::Type::DICTIONARY);
for (const auto& entry : manifest_file_handlers) {
base::Value file_handler(base::Value::Type::DICTIONARY);
file_handler.SetKey(keys::kFileHandlerIncludeDirectories,
base::Value(false));
file_handler.SetKey(keys::kFileHandlerVerb,
base::Value(apps::file_handler_verbs::kOpenWith));
base::Value mime_types(base::Value::Type::LIST);
base::Value file_extensions(base::Value::Type::LIST);
for (const auto& it : entry.accept) {
std::string type = base::UTF16ToUTF8(it.first);
if (type.empty())
continue;
mime_types.Append(base::Value(type));
for (const auto& extensionUTF16 : it.second) {
std::string extension = base::UTF16ToUTF8(extensionUTF16);
if (!IsValidFileExtension(extension))
continue;
// Remove the '.' before appending.
file_extensions.Append(base::Value(extension.substr(1)));
}
}
file_handler.SetKey(keys::kFileHandlerTypes, std::move(mime_types));
file_handler.SetKey(keys::kFileHandlerExtensions,
std::move(file_extensions));
file_handlers.SetKey(entry.action.spec(), std::move(file_handler));
}
return file_handlers;
}
base::Value CreateWebAppFileHandlersForBookmarkApp(
const std::vector<blink::Manifest::FileHandler>& manifest_file_handlers) {
base::Value file_handlers(base::Value::Type::LIST);
for (const auto& manifest_file_handler : manifest_file_handlers) {
base::Value file_handler(base::Value::Type::DICTIONARY);
base::Value accept(base::Value::Type::DICTIONARY);
for (const auto& manifest_accept_entry : manifest_file_handler.accept) {
std::string mime_type = base::UTF16ToUTF8(manifest_accept_entry.first);
if (mime_type.empty())
continue;
base::Value file_extensions(base::Value::Type::LIST);
for (const auto& manifest_file_extension : manifest_accept_entry.second) {
std::string file_extension = base::UTF16ToUTF8(manifest_file_extension);
if (!IsValidFileExtension(file_extension))
continue;
file_extensions.Append(base::Value(file_extension));
}
accept.SetKey(std::move(mime_type), std::move(file_extensions));
}
file_handler.SetKey(keys::kWebAppFileHandlerAction,
base::Value(manifest_file_handler.action.spec()));
file_handler.SetKey(keys::kWebAppFileHandlerAccept, std::move(accept));
file_handlers.Append(std::move(file_handler));
}
return file_handlers;
}
} // namespace
std::unique_ptr<base::DictionaryValue> CreateURLHandlersForBookmarkApp(
const GURL& scope_url,
const base::string16& title) {
auto matches = std::make_unique<base::ListValue>();
matches->AppendString(scope_url.GetOrigin().Resolve(scope_url.path()).spec() +
"*");
auto scope_handler = std::make_unique<base::DictionaryValue>();
scope_handler->SetList(keys::kMatches, std::move(matches));
// The URL handler title is not used anywhere but we set it to the
// web app's title just in case.
scope_handler->SetString(keys::kUrlHandlerTitle, base::UTF16ToUTF8(title));
auto url_handlers = std::make_unique<base::DictionaryValue>();
// Use "scope" as the url handler's identifier.
url_handlers->SetDictionary(kScopeUrlHandlerId, std::move(scope_handler));
return url_handlers;
}
GURL GetScopeURLFromBookmarkApp(const Extension* extension) {
DCHECK(extension->from_bookmark());
const std::vector<UrlHandlerInfo>* url_handlers =
UrlHandlers::GetUrlHandlers(extension);
if (!url_handlers)
return GURL();
// A Bookmark app created by us should only have a url_handler with id
// kScopeUrlHandlerId. This URL handler should have a single pattern which
// corresponds to the web manifest's scope. The URL handler's pattern should
// be the Web Manifest's scope's origin + path with a wildcard, '*', appended
// to it.
auto handler_it = std::find_if(
url_handlers->begin(), url_handlers->end(),
[](const UrlHandlerInfo& info) { return info.id == kScopeUrlHandlerId; });
if (handler_it == url_handlers->end()) {
return GURL();
}
const auto& patterns = handler_it->patterns;
DCHECK(patterns.size() == 1);
const auto& pattern_iter = patterns.begin();
// Remove the '*' character at the end (which was added when creating the URL
// handler, see CreateURLHandlersForBookmarkApp()).
const std::string& pattern_str = pattern_iter->GetAsString();
DCHECK_EQ(pattern_str.back(), '*');
return GURL(pattern_str.substr(0, pattern_str.size() - 1));
}
// Generates a version for the converted app using the current date. This isn't
// really needed, but it seems like useful information.
std::string ConvertTimeToExtensionVersion(const base::Time& create_time) {
base::Time::Exploded create_time_exploded;
create_time.UTCExplode(&create_time_exploded);
double micros = static_cast<double>(
(create_time_exploded.millisecond *
base::Time::kMicrosecondsPerMillisecond) +
(create_time_exploded.second * base::Time::kMicrosecondsPerSecond) +
(create_time_exploded.minute * base::Time::kMicrosecondsPerMinute) +
(create_time_exploded.hour * base::Time::kMicrosecondsPerHour));
double day_fraction = micros / base::Time::kMicrosecondsPerDay;
int stamp =
gfx::ToRoundedInt(day_fraction * std::numeric_limits<uint16_t>::max());
return base::StringPrintf("%i.%i.%i.%i", create_time_exploded.year,
create_time_exploded.month,
create_time_exploded.day_of_month, stamp);
}
scoped_refptr<Extension> ConvertWebAppToExtension(
const WebApplicationInfo& web_app,
const base::Time& create_time,
const base::FilePath& extensions_dir,
int extra_creation_flags,
Manifest::Location install_source) {
base::FilePath install_temp_dir =
file_util::GetInstallTempDir(extensions_dir);
if (install_temp_dir.empty()) {
LOG(ERROR) << "Could not get path to profile temporary directory.";
return nullptr;
}
base::ScopedTempDir temp_dir;
if (!temp_dir.CreateUniqueTempDirUnderPath(install_temp_dir)) {
LOG(ERROR) << "Could not create temporary directory.";
return nullptr;
}
// Create the manifest
std::unique_ptr<base::DictionaryValue> root(new base::DictionaryValue);
root->SetString(keys::kPublicKey,
web_app::GenerateAppKeyFromURL(web_app.app_url));
root->SetString(keys::kName, base::UTF16ToUTF8(web_app.title));
root->SetString(keys::kVersion, ConvertTimeToExtensionVersion(create_time));
root->SetString(keys::kDescription, base::UTF16ToUTF8(web_app.description));
root->SetString(keys::kLaunchWebURL, web_app.app_url.spec());
if (web_app.generated_icon_color != SK_ColorTRANSPARENT) {
root->SetString(keys::kAppIconColor, image_util::GenerateHexColorString(
web_app.generated_icon_color));
}
if (web_app.theme_color) {
root->SetString(keys::kAppThemeColor, color_utils::SkColorToRgbaString(
web_app.theme_color.value()));
}
// Currently Bookmark Apps don't support chrome-untrusted:// URLHandlers.
// Adding support for chrome-untrusted:// to URLHandlers would involve adding
// chrome-untrusted:// as a valid scheme for Extensions which has unfortunate
// side effects, like making chrome-untrusted:// URLs scriptable. Since
// Bookmark Apps are being deprecated, just don't add URLHandlers instead of
// adding support for chrome-untrusted:// and dealing with the side-effects.
#if DCHECK_IS_ON()
// Not setting URLHandlers for chrome-untrusted:// apps means that the app's
// scope will fallback to the parent directory of the start URL, which in the
// case of all SWAs today is equal to the scope they set. This DCHECK ensure
// we notice if this changes.
if (!web_app.scope.is_empty() &&
web_app.app_url.SchemeIs(content::kChromeUIUntrustedScheme)) {
DCHECK_EQ(web_app.app_url.GetWithoutFilename(), web_app.scope);
}
#endif // DCHECK_IS_ON()
if (!web_app.scope.is_empty() &&
!web_app.app_url.SchemeIs(content::kChromeUIUntrustedScheme)) {
root->SetDictionary(keys::kUrlHandlers, CreateURLHandlersForBookmarkApp(
web_app.scope, web_app.title));
}
DCHECK_NE(blink::mojom::DisplayMode::kUndefined, web_app.display_mode);
root->SetString(keys::kAppDisplayMode,
blink::DisplayModeToString(web_app.display_mode));
// TODO(crbug.com/938103): The app's file handlers are serialized twice here,
// as apps::FileHandlerInfo and apps::FileHandler. This is clearly redundant,
// but only a temporary measure, until web apps move off Bookmark Apps with
// the launch of BMO (at which point the apps::FileHandlerInfo representation
// can be removed).
if (web_app.file_handlers.size() != 0) {
root->SetKey(keys::kFileHandlers,
CreateFileHandlersForBookmarkApp(web_app.file_handlers));
root->SetKey(keys::kWebAppFileHandlers,
CreateWebAppFileHandlersForBookmarkApp(web_app.file_handlers));
}
// Add the icons and linked icon information.
auto linked_icons = std::make_unique<base::ListValue>();
for (const WebApplicationIconInfo& icon_info : web_app.icon_infos) {
DCHECK(icon_info.url.is_valid());
std::unique_ptr<base::DictionaryValue> linked_icon(
new base::DictionaryValue());
linked_icon->SetString(keys::kLinkedAppIconURL, icon_info.url.spec());
linked_icon->SetInteger(keys::kLinkedAppIconSize, icon_info.square_size_px);
linked_icons->Append(std::move(linked_icon));
}
auto icons = std::make_unique<base::DictionaryValue>();
for (const std::pair<const SquareSizePx, SkBitmap>& icon :
web_app.icon_bitmaps) {
std::string size = base::StringPrintf("%i", icon.first);
std::string icon_path = base::StringPrintf("%s/%s.png", kIconsDirName,
size.c_str());
icons->SetString(size, icon_path);
}
root->Set(keys::kIcons, std::move(icons));
root->Set(keys::kLinkedAppIcons, std::move(linked_icons));
// Add shortcuts icons and linked shortcut items information.
if (base::FeatureList::IsEnabled(
features::kDesktopPWAsAppIconShortcutsMenu) &&
!web_app.shortcut_infos.empty()) {
// |linked_shortcut_items| is a list of all entries in the Web App
// Manifest's shortcuts member. It includes the name, url and list of
// shortcut_icon_infos associated with the shortcut item.
auto linked_shortcut_items = std::make_unique<base::ListValue>();
for (const auto& shortcut : web_app.shortcut_infos) {
auto linked_shortcut_item = std::make_unique<base::DictionaryValue>();
linked_shortcut_item->SetString(keys::kWebAppLinkedShortcutItemName,
shortcut.name);
linked_shortcut_item->SetString(keys::kWebAppLinkedShortcutItemURL,
shortcut.url.spec());
// Add shortcut item icons information.
auto shortcut_item_icons = std::make_unique<base::ListValue>();
for (const auto& icon : shortcut.shortcut_icon_infos) {
DCHECK(icon.url.is_valid());
std::unique_ptr<base::DictionaryValue> shortcut_item_icon(
new base::DictionaryValue());
shortcut_item_icon->SetString(keys::kWebAppLinkedShortcutItemIconURL,
icon.url.spec());
shortcut_item_icon->SetInteger(keys::kWebAppLinkedShortcutItemIconSize,
icon.square_size_px);
shortcut_item_icons->Append(std::move(shortcut_item_icon));
}
linked_shortcut_item->Set(keys::kWebAppLinkedShortcutItemIcons,
std::move(shortcut_item_icons));
linked_shortcut_items->Append(std::move(linked_shortcut_item));
}
// |shortcuts_icons| is a mapping of 'shortcuts_icons' specified in the
// WebAppManifest and written to disk, keyed to each index in the
// WebAppManifest's shortcuts vector.
auto shortcuts_icons = std::make_unique<base::DictionaryValue>();
for (const auto& shortcut_icon_bitmaps :
web_app.shortcuts_menu_icons_bitmaps) {
// |shortcut_icons| is a mapping of filepath keyed to SquareSizePx
// specified in the WebAppManifest for every icon written to disk for the
// current shortcut in web_app.shortcut_infos. A shortcut in the
// WebAppManifest can have different icons for different sizes.
auto shortcut_icons = std::make_unique<base::DictionaryValue>();
std::string curr_icon = base::NumberToString(shortcuts_icons->size());
for (const auto& icon : shortcut_icon_bitmaps) {
std::string size = base::NumberToString(icon.first);
std::string icon_path =
base::StringPrintf("%s/%s/%s.png", kShortcutIconsDirName,
curr_icon.c_str(), size.c_str());
shortcut_icons->SetString(size, icon_path);
}
shortcuts_icons->SetDictionary(curr_icon, std::move(shortcut_icons));
}
if (!shortcuts_icons->empty())
root->Set(keys::kWebAppShortcutIcons, std::move(shortcuts_icons));
if (!linked_shortcut_items->empty()) {
root->Set(keys::kWebAppLinkedShortcutItems,
std::move(linked_shortcut_items));
}
}
// Write the manifest.
base::FilePath manifest_path = temp_dir.GetPath().Append(kManifestFilename);
JSONFileValueSerializer serializer(manifest_path);
if (!serializer.Serialize(*root)) {
LOG(ERROR) << "Could not serialize manifest.";
return nullptr;
}
// Write the icon files.
base::FilePath icons_dir = temp_dir.GetPath().AppendASCII(kIconsDirName);
if (!base::CreateDirectory(icons_dir)) {
LOG(ERROR) << "Could not create icons directory.";
return nullptr;
}
for (const std::pair<const SquareSizePx, SkBitmap>& icon :
web_app.icon_bitmaps) {
DCHECK_NE(icon.second.colorType(), kUnknown_SkColorType);
base::FilePath icon_file =
icons_dir.AppendASCII(base::StringPrintf("%i.png", icon.first));
std::vector<unsigned char> image_data;
if (!gfx::PNGCodec::EncodeBGRASkBitmap(icon.second, false, &image_data)) {
LOG(ERROR) << "Could not create icon file.";
return nullptr;
}
const char* image_data_ptr = reinterpret_cast<const char*>(&image_data[0]);
int size = base::checked_cast<int>(image_data.size());
if (base::WriteFile(icon_file, image_data_ptr, size) != size) {
LOG(ERROR) << "Could not write icon file.";
return nullptr;
}
}
// Write the shortcut icon files.
if (base::FeatureList::IsEnabled(
features::kDesktopPWAsAppIconShortcutsMenu) &&
!web_app.shortcut_infos.empty()) {
base::FilePath shortcut_icons_dir =
temp_dir.GetPath().AppendASCII(kShortcutIconsDirName);
for (size_t i = 0; i < web_app.shortcuts_menu_icons_bitmaps.size(); ++i) {
if (web_app.shortcuts_menu_icons_bitmaps[i].empty())
continue;
base::FilePath icon_dir =
shortcut_icons_dir.AppendASCII(base::NumberToString(i));
if (!base::CreateDirectory(icon_dir)) {
return nullptr;
}
for (const std::pair<const SquareSizePx, SkBitmap>& icon :
web_app.shortcuts_menu_icons_bitmaps[i]) {
DCHECK_NE(icon.second.colorType(), kUnknown_SkColorType);
base::FilePath icon_file =
icon_dir.AppendASCII(base::NumberToString(icon.first) + ".png");
std::vector<unsigned char> image_data;
if (!gfx::PNGCodec::EncodeBGRASkBitmap(icon.second, false,
&image_data)) {
return nullptr;
}
const char* image_data_ptr =
reinterpret_cast<const char*>(&image_data[0]);
int size = base::checked_cast<int>(image_data.size());
if (base::WriteFile(icon_file, image_data_ptr, size) != size) {
return nullptr;
}
}
}
}
// Finally, create the extension object to represent the unpacked directory.
std::string error;
scoped_refptr<Extension> extension = Extension::Create(
temp_dir.GetPath(), install_source, *root,
Extension::FROM_BOOKMARK | extra_creation_flags, &error);
if (!extension.get()) {
LOG(ERROR) << error;
return nullptr;
}
temp_dir.Take(); // The caller takes ownership of the directory.
return extension;
}
} // namespace extensions