Commit 68e87810 authored by Alan Cutter's avatar Alan Cutter Committed by Chromium LUCI CQ

desktop-pwas: Store capture_links manifest field in web app installations

This CL enables parsing and storing the capture_links field in web app
manifests while the kWebAppEnableLinkCapturing feature flag is enabled.

This CL is implementing the parsing side of this spec:
https://github.com/WICG/sw-launch/blob/master/declarative_link_capturing.md#proposal
including the following pending pull request to that spec:
https://github.com/WICG/sw-launch/pull/26

This CL does not implement the link capturing behaviours, that will
come in follow up CLs.

Bug: 1163398
Change-Id: I803ad87ea219ebceed35a547695a517020918c46
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2623453Reviewed-by: default avatarKentaro Hara <haraken@chromium.org>
Reviewed-by: default avatarDominick Ng <dominickn@chromium.org>
Reviewed-by: default avatarMatt Giuca <mgiuca@chromium.org>
Reviewed-by: default avatarDaniel Murphy <dmurph@chromium.org>
Commit-Queue: Alan Cutter <alancutter@chromium.org>
Cr-Commit-Position: refs/heads/master@{#843335}
parent 11e4dc99
......@@ -285,6 +285,8 @@ void UpdateWebAppInfoFromManifest(const blink::Manifest& manifest,
web_app_info->shortcuts_menu_item_infos =
UpdateShortcutsMenuItemInfosFromManifest(manifest.shortcuts);
}
web_app_info->capture_links = manifest.capture_links;
}
std::vector<GURL> GetValidIconUrlsToDownload(
......
......@@ -189,6 +189,11 @@ struct WebApplicationInfo {
// User preference as to whether to auto run the app on OS login.
// Currently only supported in Windows platform.
bool run_on_os_login = false;
// The link capturing behaviour to use for navigations into in the app's
// scope.
blink::mojom::CaptureLinks capture_links =
blink::mojom::CaptureLinks::kUndefined;
};
std::ostream& operator<<(std::ostream& out,
......
......@@ -193,4 +193,15 @@ message WebAppProto {
repeated WebAppUrlHandlerProto url_handlers = 27;
optional ClientDataProto client_data = 28;
// This enum should be synced with |ManifestCaptureLinks| in
// third_party/blink/public/mojom/manifest/manifest.mojom
enum CaptureLinks {
// UNDEFINED if optional |capture_links| is absent.
NONE = 1;
NEW_CLIENT = 2;
EXISTING_CLIENT_NAVIGATE = 3;
}
optional CaptureLinks capture_links = 29;
}
......@@ -266,6 +266,10 @@ void WebApp::SetSyncFallbackData(SyncFallbackData sync_fallback_data) {
sync_fallback_data_ = std::move(sync_fallback_data);
}
void WebApp::SetCaptureLinks(blink::mojom::CaptureLinks capture_links) {
capture_links_ = capture_links;
}
void WebApp::SetLaunchQueryParams(
base::Optional<std::string> launch_query_params) {
launch_query_params_ = std::move(launch_query_params);
......@@ -363,6 +367,7 @@ std::ostream& operator<<(std::ostream& out, const WebApp& app) {
}
for (const apps::UrlHandlerInfo& url_handler : app.url_handlers_)
out << " url_handler: " << url_handler << std::endl;
out << " capture_links: " << app.capture_links_ << std::endl;
out << " chromeos_data: " << app.chromeos_data_.has_value() << std::endl;
if (app.chromeos_data_.has_value())
......@@ -429,6 +434,7 @@ bool WebApp::operator==(const WebApp& other) const {
app.run_on_os_login_mode_,
app.sync_fallback_data_,
app.url_handlers_,
app.capture_links_,
app.client_data_.system_web_app_data
// clang-format on
);
......
......@@ -171,6 +171,8 @@ class WebApp {
return downloaded_shortcuts_menu_icons_sizes_;
}
blink::mojom::CaptureLinks capture_links() const { return capture_links_; }
// A Web App can be installed from multiple sources simultaneously. Installs
// add a source to the app. Uninstalls remove a source from the app.
void AddSource(Source::Type source);
......@@ -223,6 +225,7 @@ class WebApp {
void SetInstallTime(const base::Time& time);
void SetRunOnOsLoginMode(RunOnOsLoginMode mode);
void SetSyncFallbackData(SyncFallbackData sync_fallback_data);
void SetCaptureLinks(blink::mojom::CaptureLinks capture_links);
// For logging and debug purposes.
bool operator==(const WebApp&) const;
......@@ -274,6 +277,8 @@ class WebApp {
RunOnOsLoginMode run_on_os_login_mode_ = RunOnOsLoginMode::kUndefined;
SyncFallbackData sync_fallback_data_;
apps::UrlHandlers url_handlers_;
blink::mojom::CaptureLinks capture_links_ =
blink::mojom::CaptureLinks::kUndefined;
ClientData client_data_;
// New fields must be added to |operator==| and |operator<<|.
};
......
......@@ -74,6 +74,33 @@ apps::ShareTarget::Enctype ProtoToEnctype(ShareTarget_Enctype enctype) {
}
}
blink::mojom::CaptureLinks ProtoToCaptureLinks(
WebAppProto::CaptureLinks capture_links) {
switch (capture_links) {
case WebAppProto_CaptureLinks_NONE:
return blink::mojom::CaptureLinks::kNone;
case WebAppProto_CaptureLinks_NEW_CLIENT:
return blink::mojom::CaptureLinks::kNewClient;
case WebAppProto_CaptureLinks_EXISTING_CLIENT_NAVIGATE:
return blink::mojom::CaptureLinks::kExistingClientNavigate;
}
}
WebAppProto::CaptureLinks CaptureLinksToProto(
blink::mojom::CaptureLinks capture_links) {
switch (capture_links) {
case blink::mojom::CaptureLinks::kUndefined:
NOTREACHED();
FALLTHROUGH;
case blink::mojom::CaptureLinks::kNone:
return WebAppProto_CaptureLinks_NONE;
case blink::mojom::CaptureLinks::kNewClient:
return WebAppProto_CaptureLinks_NEW_CLIENT;
case blink::mojom::CaptureLinks::kExistingClientNavigate:
return WebAppProto_CaptureLinks_EXISTING_CLIENT_NAVIGATE;
}
}
} // anonymous namespace
WebAppDatabase::WebAppDatabase(AbstractWebAppDatabaseFactory* database_factory,
......@@ -316,6 +343,11 @@ std::unique_ptr<WebAppProto> WebAppDatabase::CreateWebAppProto(
url_handler_proto->set_origin(url_handler.origin.Serialize());
}
if (web_app.capture_links() != blink::mojom::CaptureLinks::kUndefined)
local_data->set_capture_links(CaptureLinksToProto(web_app.capture_links()));
else
local_data->clear_capture_links();
return local_data;
}
......@@ -649,6 +681,11 @@ std::unique_ptr<WebApp> WebAppDatabase::CreateWebApp(
}
web_app->SetUrlHandlers(std::move(url_handlers));
if (local_data.has_capture_links())
web_app->SetCaptureLinks(ProtoToCaptureLinks(local_data.capture_links()));
else
web_app->SetCaptureLinks(blink::mojom::CaptureLinks::kUndefined);
return web_app;
}
......
......@@ -163,6 +163,11 @@ class WebAppDatabaseTest : public WebAppTest {
return url_handlers;
}
static blink::mojom::CaptureLinks CreateCaptureLinks(uint32_t suffix) {
return static_cast<blink::mojom::CaptureLinks>(
suffix % static_cast<uint32_t>(blink::mojom::CaptureLinks::kMaxValue));
}
static std::vector<WebApplicationShortcutsMenuItemInfo>
CreateShortcutsMenuItemInfos(const std::string& base_url, uint32_t suffix) {
std::vector<WebApplicationShortcutsMenuItemInfo> shortcuts_menu_item_infos;
......@@ -302,6 +307,7 @@ class WebAppDatabaseTest : public WebAppTest {
app->SetShareTarget(CreateShareTarget(random.next_uint()));
app->SetProtocolHandlers(CreateProtocolHandlers(random.next_uint()));
app->SetUrlHandlers(CreateUrlHandlers(random.next_uint()));
app->SetCaptureLinks(CreateCaptureLinks(random.next_uint()));
const int num_additional_search_terms = random.next_uint(8);
std::vector<std::string> additional_search_terms(
......@@ -755,4 +761,19 @@ TEST_F(WebAppDatabaseTest, WebAppWithShareTargetRoundTrip) {
EXPECT_TRUE(IsRegistryEqual(mutable_registrar().registry(), registry));
}
TEST_F(WebAppDatabaseTest, WebAppWithCaptureLinksRoundTrip) {
controller().Init();
const std::string base_url = "https://example.com/path";
auto app = CreateWebApp(base_url, 0);
auto app_id = app->app_id();
app->SetCaptureLinks(blink::mojom::CaptureLinks::kExistingClientNavigate);
controller().RegisterApp(std::move(app));
Registry registry = database_factory().ReadRegistry();
EXPECT_TRUE(IsRegistryEqual(mutable_registrar().registry(), registry));
}
} // namespace web_app
......@@ -150,6 +150,8 @@ void SetWebAppManifestFields(const WebApplicationInfo& web_app_info,
// default (windowed).
web_app.SetRunOnOsLoginMode(RunOnOsLoginMode::kWindowed);
}
web_app.SetCaptureLinks(web_app_info.capture_links);
}
} // namespace web_app
......@@ -113,6 +113,9 @@ bool StructTraits<blink::mojom::ManifestDataView, ::blink::Manifest>::Read(
if (!data.ReadScope(&out->scope))
return false;
if (!data.ReadCaptureLinks(&out->capture_links))
return false;
return true;
}
......
......@@ -5,6 +5,7 @@
#include "third_party/blink/public/common/manifest/manifest_util.h"
#include "base/strings/string_util.h"
#include "third_party/blink/public/mojom/manifest/capture_links.mojom.h"
#include "third_party/blink/public/mojom/manifest/display_mode.mojom.h"
namespace blink {
......@@ -98,4 +99,14 @@ device::mojom::ScreenOrientationLockType WebScreenOrientationLockTypeFromString(
return device::mojom::ScreenOrientationLockType::DEFAULT;
}
mojom::CaptureLinks CaptureLinksFromString(const std::string& capture_links) {
if (base::LowerCaseEqualsASCII(capture_links, "none"))
return mojom::CaptureLinks::kNone;
if (base::LowerCaseEqualsASCII(capture_links, "new-client"))
return mojom::CaptureLinks::kNewClient;
if (base::LowerCaseEqualsASCII(capture_links, "existing-client-navigate"))
return mojom::CaptureLinks::kExistingClientNavigate;
return mojom::CaptureLinks::kUndefined;
}
} // namespace blink
......@@ -4,6 +4,7 @@
#include "third_party/blink/public/common/manifest/manifest_util.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/manifest/capture_links.mojom.h"
#include "third_party/blink/public/mojom/manifest/display_mode.mojom.h"
#include "url/gurl.h"
......@@ -79,4 +80,22 @@ TEST(ManifestUtilTest, WebScreenOrientationLockTypeConversions) {
WebScreenOrientationLockTypeFromString("random"));
}
TEST(ManifestUtilTest, CaptureLinksFromString) {
EXPECT_EQ(blink::mojom::CaptureLinks::kUndefined, CaptureLinksFromString(""));
EXPECT_EQ(blink::mojom::CaptureLinks::kNone, CaptureLinksFromString("none"));
EXPECT_EQ(blink::mojom::CaptureLinks::kNewClient,
CaptureLinksFromString("new-client"));
EXPECT_EQ(blink::mojom::CaptureLinks::kExistingClientNavigate,
CaptureLinksFromString("existing-client-navigate"));
// CaptureLinksFromString() should work with non-lowercase strings.
EXPECT_EQ(blink::mojom::CaptureLinks::kNewClient,
CaptureLinksFromString("NEW-CLIENT"));
// CaptureLinksFromString() should return CaptureLinks::kUndefined if the
// string isn't known.
EXPECT_EQ(blink::mojom::CaptureLinks::kUndefined,
CaptureLinksFromString("unknown-value"));
}
} // namespace blink
......@@ -237,6 +237,8 @@ struct BLINK_COMMON_EXPORT Manifest {
// document URL if start URL isn't present) with filename, query, and fragment
// removed.
GURL scope;
mojom::CaptureLinks capture_links = mojom::CaptureLinks::kUndefined;
};
} // namespace blink
......
......@@ -152,6 +152,11 @@ struct BLINK_COMMON_EXPORT
return manifest.prefer_related_applications;
}
static blink::mojom::CaptureLinks capture_links(
const ::blink::Manifest& manifest) {
return manifest.capture_links;
}
static bool Read(blink::mojom::ManifestDataView data, ::blink::Manifest* out);
};
......
......@@ -9,6 +9,7 @@
#include "services/device/public/mojom/screen_orientation_lock_types.mojom-shared.h"
#include "third_party/blink/public/common/common_export.h"
#include "third_party/blink/public/mojom/manifest/capture_links.mojom-forward.h"
#include "third_party/blink/public/mojom/manifest/display_mode.mojom-forward.h"
namespace blink {
......@@ -48,6 +49,9 @@ BLINK_COMMON_EXPORT std::string WebScreenOrientationLockTypeToString(
BLINK_COMMON_EXPORT device::mojom::ScreenOrientationLockType
WebScreenOrientationLockTypeFromString(const std::string& orientation);
BLINK_COMMON_EXPORT mojom::CaptureLinks CaptureLinksFromString(
const std::string& capture_links);
} // namespace blink
#endif // THIRD_PARTY_BLINK_PUBLIC_COMMON_MANIFEST_MANIFEST_UTIL_H_
......@@ -106,6 +106,7 @@ mojom("mojom_platform") {
"loader/transferrable_url_loader.mojom",
"loader/url_loader_factory_bundle.mojom",
"locks/lock_manager.mojom",
"manifest/capture_links.mojom",
"manifest/display_mode.mojom",
"manifest/manifest.mojom",
"manifest/manifest_manager.mojom",
......
// Copyright 2021 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.
module blink.mojom;
enum CaptureLinks {
// Used when unset or invalid.
kUndefined,
// Standard link capturing behaviours described by:
// https://github.com/WICG/sw-launch/blob/master/declarative_link_capturing.md#proposal
// No link capturing.
kNone,
// Navigations into app scope are captured into a new app tab/window.
kNewClient,
// Navigations into app scope navigate an existing app tab/window if one
// exists otherwise falls back to kNewClient.
kExistingClientNavigate,
};
......@@ -7,6 +7,7 @@ module blink.mojom;
import "services/device/public/mojom/screen_orientation_lock_types.mojom";
import "mojo/public/mojom/base/string16.mojom";
import "third_party/blink/public/mojom/manifest/display_mode.mojom";
import "third_party/blink/public/mojom/manifest/capture_links.mojom";
import "ui/gfx/geometry/mojom/geometry.mojom";
import "url/mojom/url.mojom";
import "url/mojom/origin.mojom";
......@@ -44,7 +45,7 @@ struct Manifest {
// TODO(crbug.com/829689): This field is non-standard and part of a Chrome
// experiment. See:
// https://github.com/WICG/file-handling/blob/master/explainer.md
// As such, this field should not be exposed to the drive-by web.
// As such, this field should not be exposed by default.
array<ManifestFileHandler> file_handlers;
// TODO(crbug.com/1019239): This is going into the mainline manifest spec,
......@@ -73,6 +74,12 @@ struct Manifest {
mojo_base.mojom.String16? gcm_sender_id;
url.mojom.Url scope;
// TODO(crbug.com/1163398): This field is non-standard and part of a Chrome
// experiment. See:
// https://github.com/WICG/sw-launch/blob/master/declarative_link_capturing.md#proposal
// As such, this field should not be exposed by default.
CaptureLinks capture_links;
};
// Structure representing a Shortcut Item per the Manifest specification, see:
......
......@@ -117,6 +117,7 @@ void ManifestParser::Parse() {
manifest_->gcm_sender_id = ParseGCMSenderID(root_object.get());
manifest_->shortcuts = ParseShortcuts(root_object.get());
manifest_->capture_links = ParseCaptureLinks(root_object.get());
ManifestUmaUtil::ParseSucceeded(manifest_);
}
......@@ -1214,6 +1215,48 @@ String ManifestParser::ParseGCMSenderID(const JSONObject* object) {
return gcm_sender_id.has_value() ? *gcm_sender_id : String();
}
mojom::blink::CaptureLinks ManifestParser::ParseCaptureLinks(
const JSONObject* object) {
if (!base::FeatureList::IsEnabled(features::kWebAppEnableLinkCapturing))
return mojom::blink::CaptureLinks::kUndefined;
String capture_links_string;
if (object->GetString("capture_links", &capture_links_string)) {
mojom::blink::CaptureLinks capture_links =
CaptureLinksFromString(capture_links_string.Utf8());
if (capture_links == mojom::blink::CaptureLinks::kUndefined) {
AddErrorInfo("capture_links value '" + capture_links_string +
"' ignored, unknown value.");
}
return capture_links;
}
if (JSONArray* list = object->GetArray("capture_links")) {
for (wtf_size_t i = 0; i < list->size(); ++i) {
const JSONValue* item = list->at(i);
if (!item->AsString(&capture_links_string)) {
AddErrorInfo("capture_links value '" + item->ToJSONString() +
"' ignored, string expected.");
continue;
}
mojom::blink::CaptureLinks capture_links =
CaptureLinksFromString(capture_links_string.Utf8());
if (capture_links != mojom::blink::CaptureLinks::kUndefined)
return capture_links;
AddErrorInfo("capture_links value '" + capture_links_string +
"' ignored, unknown value.");
}
return mojom::blink::CaptureLinks::kUndefined;
}
AddErrorInfo(
"property 'capture_links' ignored, type string or array of strings "
"expected.");
return mojom::blink::CaptureLinks::kUndefined;
}
void ManifestParser::AddErrorInfo(const String& error_msg,
bool critical,
int error_line,
......
......@@ -387,6 +387,11 @@ class MODULES_EXPORT ManifestParser {
// Returns the parsed string if any, a null string if the parsing failed.
String ParseGCMSenderID(const JSONObject* object);
// Parses the 'capture_links' field of the manifest.
// This specifies how navigations into the web app's scope should be captured.
// https://github.com/WICG/sw-launch/blob/master/declarative_link_capturing.md#proposal
mojom::blink::CaptureLinks ParseCaptureLinks(const JSONObject* object);
void AddErrorInfo(const String& error_msg,
bool critical = false,
int error_line = 0,
......
......@@ -4050,4 +4050,120 @@ TEST_F(ManifestParserTest, GCMSenderIDParseRules) {
}
}
TEST_F(ManifestParserTest, CaptureLinksParseRules) {
// Feature not enabled, should not be parsed.
{
auto& manifest = ParseManifest(R"({ "capture_links": "none" })");
EXPECT_EQ(manifest->capture_links, mojom::blink::CaptureLinks::kUndefined);
EXPECT_EQ(0u, GetErrorCount());
}
}
class ManifestCaptureLinksParserTest : public ManifestParserTest {
public:
ManifestCaptureLinksParserTest() {
scoped_feature_list_.InitAndEnableFeature(
features::kWebAppEnableLinkCapturing);
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(ManifestCaptureLinksParserTest, CaptureLinksParseRules) {
// Smoke test.
{
auto& manifest = ParseManifest(R"({ "capture_links": "none" })");
EXPECT_EQ(manifest->capture_links, mojom::blink::CaptureLinks::kNone);
EXPECT_EQ(0u, GetErrorCount());
}
{
auto& manifest = ParseManifest(R"({ "capture_links": ["new-client"] })");
EXPECT_EQ(manifest->capture_links, mojom::blink::CaptureLinks::kNewClient);
EXPECT_EQ(0u, GetErrorCount());
}
// Empty array is fine.
{
auto& manifest = ParseManifest(R"({ "capture_links": [] })");
EXPECT_EQ(manifest->capture_links, mojom::blink::CaptureLinks::kUndefined);
EXPECT_EQ(0u, GetErrorCount());
}
// Unknown single string.
{
auto& manifest = ParseManifest(R"({ "capture_links": "unknown" })");
EXPECT_EQ(manifest->capture_links, mojom::blink::CaptureLinks::kUndefined);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("capture_links value 'unknown' ignored, unknown value.",
errors()[0]);
}
// First known value in array is used.
{
auto& manifest = ParseManifest(
R"({ "capture_links": ["none", "existing-client-navigate"] })");
EXPECT_EQ(manifest->capture_links, mojom::blink::CaptureLinks::kNone);
EXPECT_EQ(0u, GetErrorCount());
}
{
auto& manifest = ParseManifest(R"({
"capture_links": [
"unknown",
"existing-client-navigate",
"also-unknown",
"none"
]
})");
EXPECT_EQ(manifest->capture_links,
mojom::blink::CaptureLinks::kExistingClientNavigate);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("capture_links value 'unknown' ignored, unknown value.",
errors()[0]);
}
{
auto& manifest = ParseManifest(R"({
"capture_links": [
1234,
"new-client",
null,
"none"
]
})");
EXPECT_EQ(manifest->capture_links, mojom::blink::CaptureLinks::kNewClient);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("capture_links value '1234' ignored, string expected.",
errors()[0]);
}
// Don't parse if the property isn't a string or array of strings.
{
auto& manifest = ParseManifest(R"({ "capture_links": null })");
EXPECT_EQ(manifest->capture_links, mojom::blink::CaptureLinks::kUndefined);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'capture_links' ignored, type string or array of strings "
"expected.",
errors()[0]);
}
{
auto& manifest = ParseManifest(R"({ "capture_links": 1234 })");
EXPECT_EQ(manifest->capture_links, mojom::blink::CaptureLinks::kUndefined);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'capture_links' ignored, type string or array of strings "
"expected.",
errors()[0]);
}
{
auto& manifest = ParseManifest(R"({ "capture_links": [12, 34] })");
EXPECT_EQ(manifest->capture_links, mojom::blink::CaptureLinks::kUndefined);
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ("capture_links value '12' ignored, string expected.",
errors()[0]);
EXPECT_EQ("capture_links value '34' ignored, string expected.",
errors()[1]);
}
}
} // namespace blink
......@@ -79,6 +79,8 @@ TypeConverter<blink::Manifest, blink::mojom::blink::ManifestPtr>::Convert(
if (!input->scope.IsEmpty())
output.scope = input->scope;
output.capture_links = input->capture_links;
return output;
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment