blob: 19046c793e15621f100bc77753806230a169b5a0 [file] [log] [blame]
// Copyright 2020 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/web_applications/external_web_app_utils.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/apps/user_type_filter.h"
#include "chrome/browser/web_applications/components/external_app_install_features.h"
#include "chrome/browser/web_applications/file_utils_wrapper.h"
#include "third_party/blink/public/common/manifest/manifest_util.h"
#include "ui/gfx/codec/png_codec.h"
#if defined(OS_CHROMEOS)
#include "chromeos/constants/chromeos_switches.h"
#include "components/arc/arc_util.h"
#endif // defined(OS_CHROMEOS)
namespace web_app {
namespace {
// kAppUrl is a required string specifying a URL inside the scope of the web
// app that contains a link to the app manifest.
constexpr char kAppUrl[] = "app_url";
// kHideFromUser is an optional boolean which controls whether we add
// a shortcut to the relevant OS surface i.e. Application folder on macOS, Start
// Menu on Windows and Linux, and launcher on Chrome OS. Defaults to false if
// missing. If true, we also don't show the app in search or in app management
// on Chrome OS.
constexpr char kHideFromUser[] = "hide_from_user";
// kCreateShortcuts is an optional boolean which controls whether OS
// level shortcuts are created. On Chrome OS this controls whether the app is
// pinned to the shelf.
// The default value of kCreateShortcuts if false.
constexpr char kCreateShortcuts[] = "create_shortcuts";
// kFeatureName is an optional string parameter specifying a feature
// associated with this app. The feature must be present in
// |kExternalAppInstallFeatures| to be applicable.
// If specified:
// - if the feature is enabled, the app will be installed
// - if the feature is not enabled, the app will be removed.
constexpr char kFeatureName[] = "feature_name";
#if defined(OS_CHROMEOS)
// kDisableIfArcSupported is an optional bool which specifies whether to skip
// install of the app if the device supports Arc (Chrome OS only).
// Defaults to false.
constexpr char kDisableIfArcSupported[] = "disable_if_arc_supported";
// kDisableIfTabletFormFactor is an optional bool which specifies whether to
// skip install of the app if the device is a tablet form factor.
// This is only for Chrome OS tablets, Android does not use any of this code.
// Defaults to false.
constexpr char kDisableIfTabletFormFactor[] = "disable_if_tablet_form_factor";
#endif // defined(OS_CHROMEOS)
// kLaunchContainer is a required string which can be "window" or "tab"
// and controls what sort of container the web app is launched in.
constexpr char kLaunchContainer[] = "launch_container";
constexpr char kLaunchContainerTab[] = "tab";
constexpr char kLaunchContainerWindow[] = "window";
// kLoadAndAwaitServiceWorkerRegistration is an optional bool that specifies
// whether to fetch the |kServiceWorkerRegistrationUrl| after installation to
// allow time for the app to register its service worker. This is done as a
// second pass after install in order to not block the installation of other
// background installed apps. No fetch is made if the service worker has already
// been registered by the |kAppUrl|.
// Defaults to true.
constexpr char kLoadAndAwaitServiceWorkerRegistration[] =
"load_and_await_service_worker_registration";
// kServiceWorkerRegistrationUrl is an optional string specifying the URL to use
// for the above |kLoadAndAwaitServiceWorkerRegistration|.
// Defaults to the |kAppUrl|.
constexpr char kServiceWorkerRegistrationUrl[] =
"service_worker_registration_url";
// kUninstallAndReplace is an optional array of strings which specifies App IDs
// which the app is replacing. This will transfer OS attributes (e.g the source
// app's shelf and app list positions on ChromeOS) and then uninstall the source
// app.
constexpr char kUninstallAndReplace[] = "uninstall_and_replace";
// kOnlyUseOfflineManifest is an optional bool.
// If set to true then no network install will be attempted and the app will be
// installed using |kOfflineManifest| data. |kOfflineManifest| must be specified
// in this case.
// If set to false and |kOfflineManifest| is set then it will be used as a
// fallback manifest if the network install fails.
// Defaults to false.
constexpr char kOnlyUseOfflineManifest[] = "only_use_offline_manifest";
// kOfflineManifest is a dictionary of manifest field values to use as an
// install to avoid the expense of fetching the install URL to download the
// app's true manifest. Next time the user visits the app it will undergo a
// manifest update check and correct any differences from the site (except for
// name and start_url).
//
// Why not use blink::ManifestParser?
// blink::ManifestParser depends on substantial sections of the CSS parser which
// is infeasible to run outside of the renderer process.
constexpr char kOfflineManifest[] = "offline_manifest";
// "name" manifest value to use for offline install. Cannot be updated.
// TODO(crbug.com/1119699): Allow updating of name.
constexpr char kOfflineManifestName[] = "name";
// "start_url" manifest value to use for offline install. Cannot be updated.
// TODO(crbug.com/1119699): Allow updating of start_url.
constexpr char kOfflineManifestStartUrl[] = "start_url";
// "scope" manifest value to use for offline install.
constexpr char kOfflineManifestScope[] = "scope";
// "display" manifest value to use for offline install.
constexpr char kOfflineManifestDisplay[] = "display";
// List of PNG files in the default web app config directory to use as the
// icons for offline install. Will be installed with purpose "any".
constexpr char kOfflineManifestIconAnyPngs[] = "icon_any_pngs";
// Optional 8 value ARGB hex code to use as the "theme_color" manifest value.
// Example:
// "theme_color_argb_hex": "FFFF0000"
// is equivalent to
// "theme_color": "red"
constexpr char kOfflineManifestThemeColorArgbHex[] = "theme_color_argb_hex";
} // namespace
ExternalConfigParseResult ExternalConfigParseResult::Enabled(
ExternalInstallOptions options) {
return ExternalConfigParseResult(kEnabled, std::move(options));
}
ExternalConfigParseResult ExternalConfigParseResult::Disabled() {
return ExternalConfigParseResult(kDisabled, base::nullopt);
}
ExternalConfigParseResult ExternalConfigParseResult::Error() {
return ExternalConfigParseResult(kError, base::nullopt);
}
ExternalConfigParseResult::~ExternalConfigParseResult() = default;
ExternalConfigParseResult::ExternalConfigParseResult(
ExternalConfigParseResult&&) = default;
ExternalConfigParseResult::ExternalConfigParseResult(
Type type,
base::Optional<ExternalInstallOptions> options)
: type(type), options(std::move(options)) {
DCHECK_EQ(options.has_value(), type == kEnabled);
}
ExternalConfigParseResult ParseConfig(FileUtilsWrapper& file_utils,
const base::FilePath& dir,
const base::FilePath& file,
const std::string& user_type,
const base::Value& app_config) {
using Result = ExternalConfigParseResult;
ExternalInstallOptions options(GURL(), DisplayMode::kStandalone,
ExternalInstallSource::kExternalDefault);
options.require_manifest = true;
if (app_config.type() != base::Value::Type::DICTIONARY) {
LOG(ERROR) << file << " was not a dictionary as the top level";
return Result::Error();
}
if (!apps::UserTypeMatchesJsonUserType(
user_type, /*app_id=*/file.MaybeAsASCII(), &app_config,
/*default_user_types=*/nullptr)) {
// Already logged.
return Result::Disabled();
}
// feature_name
const base::Value* value =
app_config.FindKeyOfType(kFeatureName, base::Value::Type::STRING);
if (value) {
// TODO(crbug.com/1104696): Add metrics for whether the app was
// enabled/disabled by the feature.
const std::string& feature_name = value->GetString();
VLOG(1) << file << " checking feature " << feature_name;
if (!IsExternalAppInstallFeatureEnabled(feature_name)) {
VLOG(1) << file << " feature not enabled";
return Result::Disabled();
}
}
// app_url
value = app_config.FindKeyOfType(kAppUrl, base::Value::Type::STRING);
if (!value) {
LOG(ERROR) << file << " had a missing " << kAppUrl;
return Result::Error();
}
options.install_url = GURL(value->GetString());
if (!options.install_url.is_valid()) {
LOG(ERROR) << file << " had an invalid " << kAppUrl;
return Result::Error();
}
// hide_from_user
bool hide_from_user = false;
value = app_config.FindKey(kHideFromUser);
if (value) {
if (!value->is_bool()) {
LOG(ERROR) << file << " had an invalid " << kHideFromUser;
return Result::Error();
}
hide_from_user = value->GetBool();
}
options.add_to_applications_menu = !hide_from_user;
options.add_to_search = !hide_from_user;
options.add_to_management = !hide_from_user;
// create_shortcuts
bool create_shortcuts = false;
value = app_config.FindKey(kCreateShortcuts);
if (value) {
if (!value->is_bool()) {
LOG(ERROR) << file << " had an invalid " << kCreateShortcuts;
return Result::Error();
}
create_shortcuts = value->GetBool();
}
options.add_to_desktop = create_shortcuts;
options.add_to_quick_launch_bar = create_shortcuts;
// It doesn't make sense to hide the app and also create shortcuts for it.
DCHECK(!(hide_from_user && create_shortcuts));
#if defined(OS_CHROMEOS)
// disable_if_arc_supported
value = app_config.FindKey(kDisableIfArcSupported);
if (value) {
if (!value->is_bool()) {
LOG(ERROR) << file << " had an invalid " << kDisableIfArcSupported;
return Result::Error();
}
if (value->GetBool() && arc::IsArcAvailable())
return Result::Disabled();
}
// disable_if_tablet_form_factor
value = app_config.FindKey(kDisableIfTabletFormFactor);
if (value) {
if (!value->is_bool()) {
LOG(ERROR) << file << " had an invalid " << kDisableIfTabletFormFactor;
return Result::Error();
}
if (value->GetBool() && chromeos::switches::IsTabletFormFactor())
return Result::Disabled();
}
#endif // defined(OS_CHROMEOS)
// launch_container
value = app_config.FindKeyOfType(kLaunchContainer, base::Value::Type::STRING);
if (!value) {
LOG(ERROR) << file << " had an invalid " << kLaunchContainer;
return Result::Error();
}
std::string launch_container_str = value->GetString();
if (launch_container_str == kLaunchContainerTab) {
options.user_display_mode = DisplayMode::kBrowser;
} else if (launch_container_str == kLaunchContainerWindow) {
options.user_display_mode = DisplayMode::kStandalone;
} else {
LOG(ERROR) << file << " had an invalid " << kLaunchContainer;
return Result::Error();
}
// load_and_await_service_worker_registration
value = app_config.FindKey(kLoadAndAwaitServiceWorkerRegistration);
if (value) {
if (!value->is_bool()) {
LOG(ERROR) << file << " had an invalid "
<< kLoadAndAwaitServiceWorkerRegistration;
return Result::Error();
}
options.load_and_await_service_worker_registration = value->GetBool();
}
// service_worker_registration_url
value = app_config.FindKey(kServiceWorkerRegistrationUrl);
if (value) {
if (!options.load_and_await_service_worker_registration) {
LOG(ERROR) << file << " should not specify a "
<< kServiceWorkerRegistrationUrl << " while "
<< kLoadAndAwaitServiceWorkerRegistration << " is disabled";
}
if (!value->is_string()) {
LOG(ERROR) << file << " had an invalid " << kServiceWorkerRegistrationUrl;
return Result::Error();
}
options.service_worker_registration_url.emplace(value->GetString());
if (!options.service_worker_registration_url->is_valid()) {
LOG(ERROR) << file << " had an invalid " << kServiceWorkerRegistrationUrl;
return Result::Error();
}
}
// uninstall_and_replace
value = app_config.FindKey(kUninstallAndReplace);
if (value) {
if (!value->is_list()) {
LOG(ERROR) << file << " had an invalid " << kUninstallAndReplace;
return Result::Error();
}
base::Value::ConstListView uninstall_and_replace_values = value->GetList();
bool had_error = false;
for (const auto& app_id_value : uninstall_and_replace_values) {
if (!app_id_value.is_string()) {
had_error = true;
LOG(ERROR) << file << " had an invalid " << kUninstallAndReplace
<< " entry";
break;
}
options.uninstall_and_replace.push_back(app_id_value.GetString());
}
if (had_error)
return Result::Error();
}
// only_use_offline_manifest
value = app_config.FindKey(kOnlyUseOfflineManifest);
if (value) {
if (!value->is_bool()) {
LOG(ERROR) << file << " had an invalid " << kOnlyUseOfflineManifest;
return Result::Error();
}
options.only_use_app_info_factory = value->GetBool();
}
// offline_manifest
value = app_config.FindDictKey(kOfflineManifest);
if (value) {
base::Optional<WebApplicationInfoFactory> offline_manifest_result =
ParseOfflineManifest(file_utils, dir, file, *value);
if (!offline_manifest_result.has_value()) {
// Error already logged by |ParseOfflineManifest|.
return Result::Error();
}
options.app_info_factory = std::move(offline_manifest_result.value());
}
if (options.only_use_app_info_factory && !options.app_info_factory) {
LOG(ERROR) << file << kOnlyUseOfflineManifest << " set with no "
<< kOfflineManifest << " available";
return Result::Error();
}
return Result::Enabled(std::move(options));
}
base::Optional<WebApplicationInfoFactory> ParseOfflineManifest(
FileUtilsWrapper& file_utils,
const base::FilePath& dir,
const base::FilePath& file,
const base::Value& offline_manifest) {
WebApplicationInfo app_info;
// name
const std::string* name_string =
offline_manifest.FindStringKey(kOfflineManifestName);
if (!name_string) {
LOG(ERROR) << file << " " << kOfflineManifest << " " << kOfflineManifestName
<< " missing or invalid.";
return base::nullopt;
}
if (!base::UTF8ToUTF16(name_string->data(), name_string->size(),
&app_info.title) ||
app_info.title.empty()) {
LOG(ERROR) << file << " " << kOfflineManifest << " " << kOfflineManifestName
<< " invalid: " << *name_string;
return base::nullopt;
}
// start_url
const std::string* start_url_string =
offline_manifest.FindStringKey(kOfflineManifestStartUrl);
if (!start_url_string) {
LOG(ERROR) << file << " " << kOfflineManifest << " "
<< kOfflineManifestStartUrl << " missing or invalid.";
return base::nullopt;
}
app_info.start_url = GURL(*start_url_string);
if (!app_info.start_url.is_valid()) {
LOG(ERROR) << file << " " << kOfflineManifest << " "
<< kOfflineManifestStartUrl << " invalid: " << *start_url_string;
return base::nullopt;
}
// scope
const std::string* scope_string =
offline_manifest.FindStringKey(kOfflineManifestScope);
if (!scope_string) {
LOG(ERROR) << file << " " << kOfflineManifest << " "
<< kOfflineManifestScope << " missing or invalid.";
return base::nullopt;
}
app_info.scope = GURL(*scope_string);
if (!app_info.scope.is_valid()) {
LOG(ERROR) << file << " " << kOfflineManifest << " "
<< kOfflineManifestScope << " invalid: " << *scope_string;
return base::nullopt;
}
if (!base::StartsWith(app_info.start_url.path(), app_info.scope.path(),
base::CompareCase::SENSITIVE)) {
LOG(ERROR) << file << " " << kOfflineManifest << " "
<< kOfflineManifestScope << " (" << app_info.start_url
<< ") not within " << kOfflineManifestScope << " ("
<< app_info.scope << ").";
return base::nullopt;
}
// display
const std::string* display_string =
offline_manifest.FindStringKey(kOfflineManifestDisplay);
if (!display_string) {
LOG(ERROR) << file << " " << kOfflineManifest << " "
<< kOfflineManifestDisplay << " missing or invalid.";
return base::nullopt;
}
DisplayMode display = blink::DisplayModeFromString(*display_string);
if (display == DisplayMode::kUndefined) {
LOG(ERROR) << file << " " << kOfflineManifest << " "
<< kOfflineManifestDisplay << " invalid: " << display_string;
return base::nullopt;
}
app_info.display_mode = display;
// icon_any_pngs
const base::Value* icon_files =
offline_manifest.FindListKey(kOfflineManifestIconAnyPngs);
if (!icon_files || icon_files->GetList().empty()) {
LOG(ERROR) << file << " " << kOfflineManifest << " "
<< kOfflineManifestIconAnyPngs << " missing, empty or invalid.";
return base::nullopt;
}
for (const base::Value& icon_file : icon_files->GetList()) {
if (!icon_file.is_string()) {
LOG(ERROR) << file << " " << kOfflineManifest << " "
<< kOfflineManifestIconAnyPngs << " " << icon_file
<< " invalid.";
return base::nullopt;
}
base::FilePath icon_path = dir.AppendASCII(icon_file.GetString());
std::string icon_data;
if (!file_utils.ReadFileToString(icon_path, &icon_data)) {
LOG(ERROR) << file << " " << kOfflineManifest << " "
<< kOfflineManifestIconAnyPngs << " " << icon_file
<< " failed to read.";
return base::nullopt;
}
SkBitmap bitmap;
if (!gfx::PNGCodec::Decode(
reinterpret_cast<const unsigned char*>(icon_data.c_str()),
icon_data.size(), &bitmap)) {
LOG(ERROR) << file << " " << kOfflineManifest << " "
<< kOfflineManifestIconAnyPngs << " " << icon_file
<< " failed to decode.";
return base::nullopt;
}
if (bitmap.width() != bitmap.height()) {
LOG(ERROR) << file << " " << kOfflineManifest << " "
<< kOfflineManifestIconAnyPngs << " " << icon_file
<< " must be square: " << bitmap.width() << "x"
<< bitmap.height();
return base::nullopt;
}
app_info.icon_bitmaps_any[bitmap.width()] = std::move(bitmap);
}
DCHECK(!app_info.icon_bitmaps_any.empty());
// theme_color_argb_hex (optional)
const base::Value* theme_color_value =
offline_manifest.FindKey(kOfflineManifestThemeColorArgbHex);
if (theme_color_value) {
const std::string* theme_color_argb_hex =
theme_color_value->is_string() ? &theme_color_value->GetString()
: nullptr;
SkColor theme_color;
if (!theme_color_argb_hex ||
!base::HexStringToUInt(*theme_color_argb_hex, &theme_color)) {
LOG(ERROR) << file << " " << kOfflineManifest << " "
<< kOfflineManifestThemeColorArgbHex
<< " invalid: " << *theme_color_value;
return base::nullopt;
}
app_info.theme_color = SkColorSetA(theme_color, SK_AlphaOPAQUE);
}
return base::BindRepeating(
&std::make_unique<WebApplicationInfo, const WebApplicationInfo&>,
std::move(app_info));
}
} // namespace web_app