| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <limits> |
| #include <string> |
| #include <string_view> |
| #include <vector> |
| |
| #include "base/containers/span.h" |
| #include "base/files/file.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/files/safe_base_name.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/bind.h" |
| #include "base/test/test_future.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "chrome/browser/apps/app_service/intent_util.h" |
| #include "chrome/browser/apps/app_service/launch_utils.h" |
| #include "chrome/browser/apps/link_capturing/link_capturing_feature_test_support.h" |
| #include "chrome/browser/ash/file_manager/fileapi_util.h" |
| #include "chrome/browser/ash/file_manager/path_util.h" |
| #include "chrome/browser/ash/fileapi/recent_file.h" |
| #include "chrome/browser/ash/fileapi/recent_model.h" |
| #include "chrome/browser/ash/fileapi/recent_model_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/sharesheet/sharesheet_service.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h" |
| #include "chrome/browser/ui/web_applications/web_app_browsertest_base.h" |
| #include "chrome/browser/web_applications/web_app_command_scheduler.h" |
| #include "chrome/browser/web_applications/web_app_provider.h" |
| #include "chrome/browser/web_applications/web_app_registrar.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/services/app_service/public/cpp/app_launch_util.h" |
| #include "components/services/app_service/public/cpp/intent.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" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "net/base/filename_util.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "services/network/public/cpp/resource_request_body.h" |
| #include "storage/browser/file_system/file_system_context.h" |
| #include "ui/display/types/display_constants.h" |
| #include "url/gurl.h" |
| |
| namespace { |
| |
| base::FilePath PrepareWebShareDirectory(Profile* profile) { |
| constexpr base::FilePath::CharType kWebShareDirname[] = |
| FILE_PATH_LITERAL(".WebShare"); |
| const base::FilePath directory = |
| file_manager::util::GetMyFilesFolderForProfile(profile).Append( |
| kWebShareDirname); |
| |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| base::File::Error result = base::File::FILE_OK; |
| EXPECT_TRUE(base::CreateDirectoryAndGetError(directory, &result)); |
| return directory; |
| } |
| |
| void RemoveWebShareDirectory(const base::FilePath& directory) { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| EXPECT_TRUE(base::DeletePathRecursively(directory)); |
| } |
| |
| base::FilePath StoreSharedFile(const base::FilePath& directory, |
| std::string_view name, |
| std::string_view content) { |
| const base::FilePath path = directory.Append(name); |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| base::File file(path, |
| base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE); |
| EXPECT_TRUE(file.WriteAtCurrentPosAndCheck(base::as_byte_span(content))); |
| return path; |
| } |
| |
| content::WebContents* LaunchWebAppWithIntent(Profile* profile, |
| const webapps::AppId& app_id, |
| apps::IntentPtr&& intent) { |
| apps::AppLaunchParams params = apps::CreateAppLaunchParamsForIntent( |
| app_id, |
| /*event_flags=*/0, apps::LaunchSource::kFromSharesheet, |
| display::kDefaultDisplayId, apps::LaunchContainer::kLaunchContainerWindow, |
| std::move(intent), profile); |
| |
| web_app::WebAppProvider* provider = |
| web_app::WebAppProvider::GetForLocalAppsUnchecked(profile); |
| base::test::TestFuture<base::WeakPtr<Browser>, |
| base::WeakPtr<content::WebContents>, |
| apps::LaunchContainer> |
| future; |
| provider->scheduler().LaunchAppWithCustomParams(std::move(params), |
| future.GetCallback()); |
| return future.template Get<1>().get(); |
| } |
| |
| content::EvalJsResult ReadTextContent(content::WebContents* web_contents, |
| const char* id) { |
| const std::string script = |
| base::StringPrintf("document.getElementById('%s').textContent", id); |
| return content::EvalJs(web_contents, script); |
| } |
| |
| } // namespace |
| |
| namespace web_app { |
| |
| class WebShareTargetBrowserTest : public WebAppBrowserTestBase, |
| public testing::WithParamInterface< |
| apps::test::LinkCapturingFeatureVersion> { |
| public: |
| WebShareTargetBrowserTest() { |
| scoped_feature_list_.InitWithFeaturesAndParameters( |
| apps::test::GetFeaturesToEnableLinkCapturingUX(GetParam()), {}); |
| } |
| |
| GURL share_target_url() const { |
| return embedded_test_server()->GetURL("/web_share_target/share.html"); |
| } |
| |
| content::WebContents* LaunchAppWithIntent(const webapps::AppId& app_id, |
| apps::IntentPtr&& intent, |
| const GURL& expected_url) { |
| DCHECK(intent); |
| ui_test_utils::UrlLoadObserver url_observer(expected_url); |
| |
| content::WebContents* const web_contents = |
| LaunchWebAppWithIntent(profile(), app_id, std::move(intent)); |
| url_observer.Wait(); |
| EXPECT_EQ(expected_url, web_contents->GetVisibleURL()); |
| return web_contents; |
| } |
| |
| unsigned NumRecentFiles(content::WebContents* contents) { |
| unsigned result = std::numeric_limits<unsigned>::max(); |
| base::RunLoop run_loop; |
| |
| const scoped_refptr<storage::FileSystemContext> file_system_context = |
| file_manager::util::GetFileSystemContextForRenderFrameHost( |
| profile(), contents->GetPrimaryMainFrame()); |
| ash::RecentModelOptions options; |
| options.source_specs.emplace_back(ash::RecentSourceSpec{ |
| .volume_type = |
| extensions::api::file_manager_private::VolumeType::kTesting, |
| }); |
| ash::RecentModelFactory::GetForProfile(profile())->GetRecentFiles( |
| file_system_context.get(), |
| /*origin=*/GURL(), |
| /*query=*/"", options, |
| base::BindLambdaForTesting( |
| [&result, &run_loop](const std::vector<ash::RecentFile>& files) { |
| result = files.size(); |
| run_loop.Quit(); |
| })); |
| |
| run_loop.Run(); |
| return result; |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(WebShareTargetBrowserTest, ShareUsingFileURL) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| const GURL app_url = |
| embedded_test_server()->GetURL("/web_share_target/charts.html"); |
| const webapps::AppId app_id = |
| web_app::InstallWebAppFromManifest(browser(), app_url); |
| // Enabling link capturing to ensure it doesn't interfere. |
| ASSERT_EQ(apps::test::EnableLinkCapturingByUser(profile(), app_id), |
| base::ok()); |
| |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| base::ScopedTempDir scoped_temp_dir; |
| ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir()); |
| |
| auto intent = |
| std::make_unique<apps::Intent>(apps_util::kIntentActionSendMultiple); |
| { |
| const base::FilePath first_csv = |
| StoreSharedFile(scoped_temp_dir.GetPath(), "first.csv", "1,2,3,4,5"); |
| const base::FilePath second_csv = |
| StoreSharedFile(scoped_temp_dir.GetPath(), "second.csv", "6,7,8,9,0"); |
| |
| std::vector<base::FilePath> file_paths({first_csv, second_csv}); |
| |
| intent->mime_type = "text/csv"; |
| for (const base::FilePath& file_path : file_paths) { |
| int64_t file_size = base::GetFileSize(file_path).value_or(0); |
| auto file = |
| std::make_unique<apps::IntentFile>(net::FilePathToFileURL(file_path)); |
| file->file_name = base::SafeBaseName::Create(file_path); |
| file->file_size = file_size; |
| file->mime_type = "text/csv"; |
| intent->files.push_back(std::move(file)); |
| } |
| } |
| |
| content::WebContents* const web_contents = |
| LaunchAppWithIntent(app_id, std::move(intent), share_target_url()); |
| EXPECT_EQ("1,2,3,4,5 6,7,8,9,0", ReadTextContent(web_contents, "records")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(WebShareTargetBrowserTest, ShareImageWithText) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| const GURL app_url = |
| embedded_test_server()->GetURL("/web_share_target/charts.html"); |
| const webapps::AppId app_id = |
| web_app::InstallWebAppFromManifest(browser(), app_url); |
| // Enabling link capturing to ensure it doesn't interfere. |
| ASSERT_EQ(apps::test::EnableLinkCapturingByUser(profile(), app_id), |
| base::ok()); |
| const base::FilePath directory = PrepareWebShareDirectory(profile()); |
| |
| apps::IntentPtr intent; |
| { |
| const base::FilePath first_svg = |
| StoreSharedFile(directory, "first.svg", "picture"); |
| |
| std::vector<base::FilePath> file_paths({first_svg}); |
| std::vector<std::string> content_types(1, "image/svg+xml"); |
| intent = apps_util::CreateShareIntentFromFiles( |
| profile(), std::move(file_paths), std::move(content_types), |
| /*share_text=*/"Euclid https://example.org/", |
| /*share_title=*/"Elements"); |
| } |
| |
| content::WebContents* const web_contents = |
| LaunchAppWithIntent(app_id, std::move(intent), share_target_url()); |
| EXPECT_EQ("picture", ReadTextContent(web_contents, "graphs")); |
| |
| EXPECT_EQ("Elements", ReadTextContent(web_contents, "headline")); |
| EXPECT_EQ("Euclid", ReadTextContent(web_contents, "author")); |
| EXPECT_EQ("https://example.org/", ReadTextContent(web_contents, "link")); |
| EXPECT_EQ(NumRecentFiles(web_contents), 0U); |
| |
| RemoveWebShareDirectory(directory); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(WebShareTargetBrowserTest, ShareAudio) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| const GURL app_url = |
| embedded_test_server()->GetURL("/web_share_target/charts.html"); |
| const webapps::AppId app_id = |
| web_app::InstallWebAppFromManifest(browser(), app_url); |
| // Enabling link capturing to ensure it doesn't interfere. |
| ASSERT_EQ(apps::test::EnableLinkCapturingByUser(profile(), app_id), |
| base::ok()); |
| const base::FilePath directory = PrepareWebShareDirectory(profile()); |
| |
| apps::IntentPtr intent; |
| { |
| const base::FilePath first_weba = |
| StoreSharedFile(directory, "first.weba", "a"); |
| const base::FilePath second_weba = |
| StoreSharedFile(directory, "second.weba", "b"); |
| const base::FilePath third_weba = |
| StoreSharedFile(directory, "third.weba", "c"); |
| |
| std::vector<base::FilePath> file_paths( |
| {first_weba, second_weba, third_weba}); |
| std::vector<std::string> content_types(3, "audio/webm"); |
| intent = apps_util::CreateShareIntentFromFiles( |
| profile(), std::move(file_paths), std::move(content_types)); |
| intent->share_text = ""; |
| intent->share_title = ""; |
| } |
| |
| content::WebContents* const web_contents = |
| LaunchAppWithIntent(app_id, std::move(intent), share_target_url()); |
| EXPECT_EQ("a b c", ReadTextContent(web_contents, "notes")); |
| EXPECT_EQ(NumRecentFiles(web_contents), 0U); |
| |
| RemoveWebShareDirectory(directory); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(WebShareTargetBrowserTest, PostBlank) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| const GURL app_url = |
| embedded_test_server()->GetURL("/web_share_target/poster.html"); |
| const webapps::AppId app_id = |
| web_app::InstallWebAppFromManifest(browser(), app_url); |
| |
| apps::IntentPtr intent = apps_util::MakeShareIntent( |
| /*text=*/std::string(), |
| /*title=*/std::string()); |
| |
| content::WebContents* const web_contents = |
| LaunchAppWithIntent(app_id, std::move(intent), share_target_url()); |
| // Poster web app's service worker detects omitted values. |
| EXPECT_EQ("N/A", ReadTextContent(web_contents, "headline")); |
| EXPECT_EQ("N/A", ReadTextContent(web_contents, "author")); |
| EXPECT_EQ("N/A", ReadTextContent(web_contents, "link")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(WebShareTargetBrowserTest, PostLink) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| const GURL app_url = |
| embedded_test_server()->GetURL("/web_share_target/poster.html"); |
| const webapps::AppId app_id = |
| web_app::InstallWebAppFromManifest(browser(), app_url); |
| // Enabling link capturing to ensure it doesn't interfere. |
| ASSERT_EQ(apps::test::EnableLinkCapturingByUser(profile(), app_id), |
| base::ok()); |
| const apps::ShareTarget* share_target = |
| WebAppProvider::GetForTest(browser()->profile()) |
| ->registrar_unsafe() |
| .GetAppShareTarget(app_id); |
| EXPECT_EQ(share_target->method, apps::ShareTarget::Method::kPost); |
| EXPECT_EQ(share_target->enctype, apps::ShareTarget::Enctype::kFormUrlEncoded); |
| |
| const std::string shared_title = "Hyperlink"; |
| const std::string shared_link = "https://example.org/a?b=c&d=e%20#f"; |
| |
| apps::IntentPtr intent = apps_util::MakeShareIntent( |
| /*text=*/shared_link, |
| /*title=*/shared_title); |
| |
| content::WebContents* const web_contents = |
| LaunchAppWithIntent(app_id, std::move(intent), share_target_url()); |
| EXPECT_EQ("POST", ReadTextContent(web_contents, "method")); |
| EXPECT_EQ("application/x-www-form-urlencoded", |
| ReadTextContent(web_contents, "type")); |
| |
| EXPECT_EQ(shared_title, ReadTextContent(web_contents, "headline")); |
| // Poster web app's service worker detects omitted value. |
| EXPECT_EQ("N/A", ReadTextContent(web_contents, "author")); |
| EXPECT_EQ(shared_link, ReadTextContent(web_contents, "link")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(WebShareTargetBrowserTest, GetLink) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| const GURL app_url = |
| embedded_test_server()->GetURL("/web_share_target/gatherer.html"); |
| const webapps::AppId app_id = |
| web_app::InstallWebAppFromManifest(browser(), app_url); |
| // Enabling link capturing to ensure it doesn't interfere. |
| ASSERT_EQ(apps::test::EnableLinkCapturingByUser(profile(), app_id), |
| base::ok()); |
| const apps::ShareTarget* share_target = |
| WebAppProvider::GetForTest(browser()->profile()) |
| ->registrar_unsafe() |
| .GetAppShareTarget(app_id); |
| EXPECT_EQ(share_target->method, apps::ShareTarget::Method::kGet); |
| EXPECT_EQ(share_target->enctype, apps::ShareTarget::Enctype::kFormUrlEncoded); |
| |
| const std::string shared_title = "My News"; |
| const std::string shared_link = "http://example.com/news"; |
| const GURL expected_url(share_target_url().spec() + |
| "?headline=My+News&link=http://example.com/news"); |
| |
| apps::IntentPtr intent = apps_util::MakeShareIntent( |
| /*text=*/shared_link, |
| /*title=*/shared_title); |
| |
| content::WebContents* const web_contents = |
| LaunchAppWithIntent(app_id, std::move(intent), expected_url); |
| EXPECT_EQ("GET", ReadTextContent(web_contents, "method")); |
| EXPECT_EQ(expected_url.spec(), ReadTextContent(web_contents, "url")); |
| |
| EXPECT_EQ(shared_title, ReadTextContent(web_contents, "headline")); |
| // Gatherer web app's service worker detects omitted value. |
| EXPECT_EQ("N/A", ReadTextContent(web_contents, "author")); |
| EXPECT_EQ(shared_link, ReadTextContent(web_contents, "link")); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| WebShareTargetBrowserTest, |
| // Ensure share target still works with navigation capturing v2. |
| testing::Values(apps::test::LinkCapturingFeatureVersion::kV1DefaultOff, |
| apps::test::LinkCapturingFeatureVersion::kV2DefaultOff, |
| apps::test::LinkCapturingFeatureVersion:: |
| kV2DefaultOffCaptureExistingFrames), |
| apps::test::LinkCapturingVersionToString); |
| |
| } // namespace web_app |