Commit ee5d4655 authored by Eric Willigers's avatar Eric Willigers Committed by Commit Bot

Web Share Target: CreateShareFileFilter for ChromeOS

Populates IntentFilter for PWAs with share targets.

Spec:
https://w3c.github.io/web-share-target/level-2/

Both single files (kIntentActionSend)
and multiple files (kIntentActionSendMultiple)
may be shared.

Bug: 1125880
Change-Id: I39a63cbad3bb1b765185a008cc74927152b34a2b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2434079
Commit-Queue: Eric Willigers <ericwilligers@chromium.org>
Reviewed-by: default avatarMaggie Cai <mxcai@chromium.org>
Reviewed-by: default avatarDominick Ng <dominickn@chromium.org>
Reviewed-by: default avatarAlan Cutter <alancutter@chromium.org>
Cr-Commit-Position: refs/heads/master@{#812041}
parent a8ab82e7
......@@ -31,6 +31,8 @@
#include "components/content_settings/core/common/content_settings_pattern.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/services/app_service/public/cpp/intent_filter_util.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "components/services/app_service/public/cpp/share_target.h"
#include "content/public/browser/web_contents.h"
namespace {
......@@ -59,6 +61,38 @@ apps::mojom::InstallSource GetHighestPriorityInstallSource(
}
}
apps::mojom::IntentFilterPtr CreateShareFileFilter(
const std::vector<std::string>& intent_actions,
const std::vector<std::string>& content_types) {
auto intent_filter = apps::mojom::IntentFilter::New();
std::vector<apps::mojom::ConditionValuePtr> action_condition_values;
for (auto& action : intent_actions) {
action_condition_values.push_back(apps_util::MakeConditionValue(
action, apps::mojom::PatternMatchType::kNone));
}
if (!action_condition_values.empty()) {
auto action_condition =
apps_util::MakeCondition(apps::mojom::ConditionType::kAction,
std::move(action_condition_values));
intent_filter->conditions.push_back(std::move(action_condition));
}
std::vector<apps::mojom::ConditionValuePtr> mime_type_condition_values;
for (auto& mime_type : content_types) {
mime_type_condition_values.push_back(apps_util::MakeConditionValue(
mime_type, apps::mojom::PatternMatchType::kNone));
}
if (!mime_type_condition_values.empty()) {
auto mime_type_condition =
apps_util::MakeCondition(apps::mojom::ConditionType::kMimeType,
std::move(mime_type_condition_values));
intent_filter->conditions.push_back(std::move(mime_type_condition));
}
return intent_filter;
}
} // namespace
namespace apps {
......@@ -124,8 +158,7 @@ apps::mojom::AppPtr WebAppsBase::ConvertImpl(const web_app::WebApp* web_app,
SetShowInFields(app, web_app);
// Get the intent filters for PWAs.
PopulateIntentFilters(GetRegistrar()->GetAppScope(web_app->app_id()),
&app->intent_filters);
PopulateIntentFilters(*web_app, app->intent_filters);
return app;
}
......@@ -508,16 +541,6 @@ void WebAppsBase::PopulatePermissions(
}
}
void WebAppsBase::PopulateIntentFilters(
const base::Optional<GURL>& app_scope,
std::vector<mojom::IntentFilterPtr>* target) {
if (app_scope != base::nullopt) {
target->push_back(apps_util::CreateIntentFilterForUrlScope(
app_scope.value(),
base::FeatureList::IsEnabled(features::kIntentHandlingSharing)));
}
}
void WebAppsBase::ConvertWebApps(apps::mojom::Readiness readiness,
std::vector<apps::mojom::AppPtr>* apps_out) {
const web_app::WebAppRegistrar* registrar = GetRegistrar();
......@@ -543,4 +566,48 @@ void WebAppsBase::StartPublishingWebApps(
subscribers_.Add(std::move(subscriber));
}
void PopulateIntentFilters(const web_app::WebApp& web_app,
std::vector<mojom::IntentFilterPtr>& target) {
if (web_app.scope().is_empty())
return;
target.push_back(apps_util::CreateIntentFilterForUrlScope(
web_app.scope(),
base::FeatureList::IsEnabled(features::kIntentHandlingSharing)));
if (!base::FeatureList::IsEnabled(features::kIntentHandlingSharing) ||
!web_app.share_target().has_value()) {
return;
}
const apps::ShareTarget& share_target = web_app.share_target().value();
// TODO(crbug.com/1127670): Support title/text/url sharing on ChromeOS
if (share_target.params.files.empty() ||
share_target.method != apps::ShareTarget::Method::kPost ||
share_target.enctype != apps::ShareTarget::Enctype::kMultipartFormData) {
return;
}
std::vector<std::string> content_types;
for (const auto& files_entry : share_target.params.files) {
for (const auto& file_type : files_entry.accept) {
// Skip any file_type that is not a MIME type.
if (file_type.empty() || file_type[0] == '.' ||
std::count(file_type.begin(), file_type.end(), '/') != 1) {
continue;
}
content_types.push_back(file_type);
}
}
if (content_types.empty())
return;
const std::vector<std::string> intent_actions(
{apps_util::kIntentActionSend, apps_util::kIntentActionSendMultiple});
target.push_back(CreateShareFileFilter(intent_actions, content_types));
}
} // namespace apps
......@@ -7,6 +7,7 @@
#include <memory>
#include <string>
#include <vector>
#include "base/memory/weak_ptr.h"
#include "base/scoped_observer.h"
......@@ -139,8 +140,6 @@ class WebAppsBase : public apps::PublisherBase,
const web_app::WebApp* web_app);
void PopulatePermissions(const web_app::WebApp* web_app,
std::vector<mojom::PermissionPtr>* target);
void PopulateIntentFilters(const base::Optional<GURL>& app_scope,
std::vector<mojom::IntentFilterPtr>* target);
virtual apps::mojom::AppPtr Convert(const web_app::WebApp* web_app,
apps::mojom::Readiness readiness) = 0;
void ConvertWebApps(apps::mojom::Readiness readiness,
......@@ -172,6 +171,9 @@ class WebAppsBase : public apps::PublisherBase,
base::WeakPtrFactory<WebAppsBase> weak_ptr_factory_{this};
};
void PopulateIntentFilters(const web_app::WebApp& web_app,
std::vector<mojom::IntentFilterPtr>& target);
} // namespace apps
#endif // CHROME_BROWSER_APPS_APP_SERVICE_WEB_APPS_BASE_H_
// 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/apps/app_service/web_apps_base.h"
#include <vector>
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/web_applications/components/web_app_id.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "components/services/app_service/public/mojom/types.mojom.h"
#include "content/public/test/browser_test.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "url/gurl.h"
using apps::mojom::Condition;
using apps::mojom::ConditionType;
using apps::mojom::PatternMatchType;
namespace apps {
namespace {
void CheckUrlScopeFilter(const apps::mojom::IntentFilterPtr& intent_filter,
const GURL& url,
const GURL& different_url) {
EXPECT_FALSE(intent_filter->activity_name.has_value());
EXPECT_FALSE(intent_filter->activity_label.has_value());
EXPECT_EQ(intent_filter->conditions.size(), 4U);
{
const Condition& condition = *intent_filter->conditions[0];
EXPECT_EQ(condition.condition_type, ConditionType::kAction);
EXPECT_EQ(condition.condition_values.size(), 1U);
EXPECT_EQ(condition.condition_values[0]->match_type,
PatternMatchType::kNone);
EXPECT_EQ(condition.condition_values[0]->value, "view");
}
{
const Condition& condition = *intent_filter->conditions[1];
EXPECT_EQ(condition.condition_type, ConditionType::kScheme);
EXPECT_EQ(condition.condition_values.size(), 1U);
EXPECT_EQ(condition.condition_values[0]->match_type,
PatternMatchType::kNone);
EXPECT_EQ(condition.condition_values[0]->value, url.scheme());
}
{
const Condition& condition = *intent_filter->conditions[2];
EXPECT_EQ(condition.condition_type, ConditionType::kHost);
EXPECT_EQ(condition.condition_values.size(), 1U);
EXPECT_EQ(condition.condition_values[0]->match_type,
PatternMatchType::kNone);
EXPECT_EQ(condition.condition_values[0]->value, url.host());
}
{
const Condition& condition = *intent_filter->conditions[3];
EXPECT_EQ(condition.condition_type, ConditionType::kPattern);
EXPECT_EQ(condition.condition_values.size(), 1U);
EXPECT_EQ(condition.condition_values[0]->match_type,
PatternMatchType::kPrefix);
EXPECT_EQ(condition.condition_values[0]->value, url.path());
}
EXPECT_TRUE(apps_util::IntentMatchesFilter(
apps_util::CreateIntentFromUrl(url), intent_filter));
EXPECT_FALSE(apps_util::IntentMatchesFilter(
apps_util::CreateIntentFromUrl(different_url), intent_filter));
}
void CheckShareFileFilter(const apps::mojom::IntentFilterPtr& intent_filter,
const std::vector<std::string>& content_types,
const std::string& different_content_type) {
EXPECT_FALSE(intent_filter->activity_name.has_value());
EXPECT_FALSE(intent_filter->activity_label.has_value());
EXPECT_EQ(intent_filter->conditions.size(), 2U);
{
const Condition& condition = *intent_filter->conditions[0];
EXPECT_EQ(condition.condition_type, ConditionType::kAction);
EXPECT_EQ(condition.condition_values.size(), 2U);
EXPECT_EQ(condition.condition_values[0]->match_type,
PatternMatchType::kNone);
EXPECT_EQ(condition.condition_values[0]->value, "send");
EXPECT_EQ(condition.condition_values[1]->match_type,
PatternMatchType::kNone);
EXPECT_EQ(condition.condition_values[1]->value, "send_multiple");
}
{
const Condition& condition = *intent_filter->conditions[1];
EXPECT_EQ(condition.condition_type, ConditionType::kMimeType);
EXPECT_EQ(condition.condition_values.size(), content_types.size());
for (unsigned i = 0; i < content_types.size(); ++i) {
EXPECT_EQ(condition.condition_values[i]->match_type,
PatternMatchType::kNone);
EXPECT_EQ(condition.condition_values[i]->value, content_types[i]);
{
std::vector<GURL> filesystem_urls(1U);
std::vector<std::string> mime_types(1U, content_types[i]);
EXPECT_TRUE(apps_util::IntentMatchesFilter(
apps_util::CreateShareIntentFromFiles(filesystem_urls, mime_types),
intent_filter));
}
{
std::vector<GURL> filesystem_urls(3U);
std::vector<std::string> mime_types(3U, content_types[i]);
EXPECT_TRUE(apps_util::IntentMatchesFilter(
apps_util::CreateShareIntentFromFiles(filesystem_urls, mime_types),
intent_filter));
}
}
}
{
std::vector<GURL> filesystem_urls(1U);
std::vector<std::string> mime_types(1U, different_content_type);
EXPECT_FALSE(apps_util::IntentMatchesFilter(
apps_util::CreateShareIntentFromFiles(filesystem_urls, mime_types),
intent_filter));
}
}
} // namespace
class WebAppsBaseBrowserTest : public InProcessBrowserTest {
public:
WebAppsBaseBrowserTest() {
feature_list_.InitAndEnableFeature(features::kIntentHandlingSharing);
}
~WebAppsBaseBrowserTest() override = default;
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(WebAppsBaseBrowserTest, PopulateIntentFilters) {
ASSERT_TRUE(embedded_test_server()->Start());
const GURL app_url(
embedded_test_server()->GetURL("/web_share_target/charts.html"));
std::vector<mojom::IntentFilterPtr> target;
{
const web_app::WebAppRegistrar* registrar =
web_app::WebAppProvider::Get(browser()->profile())
->registrar()
.AsWebAppRegistrar();
const web_app::AppId app_id =
web_app::InstallWebAppFromManifest(browser(), app_url);
const web_app::WebApp* web_app = registrar->GetAppById(app_id);
ASSERT_TRUE(web_app);
PopulateIntentFilters(*web_app, target);
}
EXPECT_EQ(target.size(), 2U);
CheckUrlScopeFilter(target[0], app_url.GetWithoutFilename(),
/*different_url=*/GURL("file:///"));
const std::vector<std::string> content_types({"text/csv", "image/svg+xml"});
CheckShareFileFilter(
target[1], content_types,
/*different_content_type=*/"application/vnd.android.package-archive");
}
} // namespace apps
......@@ -23,12 +23,13 @@
#include "chrome/browser/web_applications/components/app_registrar.h"
#include "chrome/browser/web_applications/components/external_install_options.h"
#include "chrome/browser/web_applications/components/install_finalizer.h"
#include "chrome/browser/web_applications/components/install_manager.h"
#include "chrome/browser/web_applications/components/pending_app_manager.h"
#include "chrome/browser/web_applications/components/web_app_constants.h"
#include "chrome/browser/web_applications/components/web_app_helpers.h"
#include "chrome/browser/web_applications/components/web_app_provider_base.h"
#include "chrome/browser/web_applications/components/web_app_tab_helper_base.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/common/web_application_info.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/security_interstitials/content/security_interstitial_tab_helper.h"
#include "content/public/browser/notification_service.h"
......@@ -64,6 +65,39 @@ AppId InstallWebApp(Profile* profile,
return app_id;
}
AppId InstallWebAppFromManifest(Browser* browser, const GURL& app_url) {
NavigateToURLAndWait(browser, app_url);
AppId app_id;
base::RunLoop run_loop;
auto* provider = WebAppProviderBase::GetProviderBase(browser->profile());
DCHECK(provider);
provider->install_manager().InstallWebAppFromManifestWithFallback(
browser->tab_strip_model()->GetActiveWebContents(),
/*force_shortcut_app=*/true, WebappInstallSource::MENU_BROWSER_TAB,
base::BindLambdaForTesting(
[](content::WebContents* initiator_web_contents,
std::unique_ptr<WebApplicationInfo> web_app_info,
ForInstallableSite for_installable_site,
InstallManager::WebAppInstallationAcceptanceCallback
acceptance_callback) {
std::move(acceptance_callback)
.Run(
/*user_accepted=*/true, std::move(web_app_info));
}),
base::BindLambdaForTesting(
[&run_loop, &app_id](const AppId& installed_app_id,
InstallResultCode code) {
DCHECK_EQ(code, InstallResultCode::kSuccessNewInstall);
app_id = installed_app_id;
run_loop.Quit();
}));
run_loop.Run();
return app_id;
}
Browser* LaunchWebAppBrowser(Profile* profile, const AppId& app_id) {
EXPECT_TRUE(
apps::AppServiceProxyFactory::GetForProfile(profile)
......@@ -84,7 +118,8 @@ Browser* LaunchWebAppBrowser(Profile* profile, const AppId& app_id) {
// Launches the app, waits for the app url to load.
Browser* LaunchWebAppBrowserAndWait(Profile* profile, const AppId& app_id) {
ui_test_utils::UrlLoadObserver url_observer(
WebAppProvider::Get(profile)->registrar().GetAppLaunchUrl(app_id),
WebAppProviderBase::GetProviderBase(profile)->registrar().GetAppLaunchUrl(
app_id),
content::NotificationService::AllSources());
Browser* const app_browser = LaunchWebAppBrowser(profile, app_id);
url_observer.Wait();
......@@ -182,7 +217,7 @@ void NavigateAndCheckForToolbar(Browser* browser,
const GURL& url,
bool expected_visibility,
bool proceed_through_interstitial) {
web_app::NavigateToURLAndWait(browser, url, proceed_through_interstitial);
NavigateToURLAndWait(browser, url, proceed_through_interstitial);
EXPECT_EQ(expected_visibility,
browser->app_controller()->ShouldShowCustomTabBar());
}
......
......@@ -23,6 +23,9 @@ enum class InstallResultCode;
// Synchronous version of InstallManager::InstallWebAppFromInfo.
AppId InstallWebApp(Profile* profile, std::unique_ptr<WebApplicationInfo>);
// Navigates to |app_url|, verifies WebApp installability, and installs app.
AppId InstallWebAppFromManifest(Browser* browser, const GURL& app_url);
// Launches a new app window for |app| in |profile|.
Browser* LaunchWebAppBrowser(Profile*, const AppId&);
......
......@@ -1663,6 +1663,7 @@ if (!is_android) {
sources += [
"../browser/apps/app_service/notifications_browsertest.cc",
"../browser/apps/app_service/web_apps_base_browsertest.cc",
"../browser/policy/accessibility_policy_browsertest.cc",
"../browser/policy/arc_policy_browsertest.cc",
"../browser/policy/assistant_policy_browsertest.cc",
......
<!DOCTYPE html>
<html>
<head>
<link rel="manifest" href="charts.json">
<link rel="icon" href="basic-48.png">
</head>
<body>
<h1>Charts web app</h1>
<script>
navigator.serviceWorker.register('/web_share_target/service_worker.js');
</script>
</body>
</html>
{
"name": "Charts web app",
"icons": [
{
"src": "basic-48.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "basic-192.png",
"sizes": "192x192",
"type": "image/png"
}
],
"start_url": "charts.html",
"display": "minimal-ui",
"share_target": {
"action": "/web_share_target/share.html",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"files": [
{
"name": "records",
"accept": ["text/csv", ".csv"]
},
{
"name": "graphs",
"accept": "image/svg+xml"
}
]
}
}
}
// 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.
'use strict';
self.addEventListener('fetch', e => {
e.respondWith((async () => {
try {
return await fetch(e.request);
} catch (error) {
return new Response('Hello offline page');
}
})());
});
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