| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/offline_pages/offline_page_tab_helper.h" |
| |
| #include <memory> |
| |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "build/build_config.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "components/back_forward_cache/back_forward_cache_disable.h" |
| #include "components/keyed_service/core/simple_key_map.h" |
| #include "components/offline_pages/core/model/offline_page_model_utils.h" |
| #include "components/offline_pages/core/offline_page_item.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/test/back_forward_cache_util.h" |
| #include "content/public/test/navigation_simulator.h" |
| #include "content/public/test/test_renderer_host.h" |
| #include "content/public/test/web_contents_tester.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/features.h" |
| |
| namespace { |
| |
| const base::Time kTestMhtmlCreationTime = |
| base::Time::FromMillisecondsSinceUnixEpoch(1522339419011L); |
| |
| const char kTestHeader[] = "reason=download"; |
| |
| } // namespace |
| |
| namespace offline_pages { |
| namespace { |
| |
| using blink::mojom::MHTMLLoadResult; |
| |
| class OfflinePageTabHelperTest : public content::RenderViewHostTestHarness { |
| public: |
| OfflinePageTabHelperTest(); |
| |
| OfflinePageTabHelperTest(const OfflinePageTabHelperTest&) = delete; |
| OfflinePageTabHelperTest& operator=(const OfflinePageTabHelperTest&) = delete; |
| |
| ~OfflinePageTabHelperTest() override {} |
| |
| void SetUp() override; |
| void TearDown() override; |
| std::unique_ptr<content::BrowserContext> CreateBrowserContext() override; |
| |
| void CreateNavigationSimulator(const GURL& url); |
| |
| void SimulateOfflinePageLoad(const GURL& mhtml_url, |
| base::Time mhtml_creation_time, |
| MHTMLLoadResult load_result); |
| |
| OfflinePageTabHelper* tab_helper() const { return tab_helper_; } |
| content::NavigationSimulator* navigation_simulator() { |
| return navigation_simulator_.get(); |
| } |
| |
| private: |
| raw_ptr<OfflinePageTabHelper> tab_helper_; // Owned by WebContents. |
| std::unique_ptr<content::NavigationSimulator> navigation_simulator_; |
| |
| base::WeakPtrFactory<OfflinePageTabHelperTest> weak_ptr_factory_{this}; |
| }; |
| |
| OfflinePageTabHelperTest::OfflinePageTabHelperTest() : tab_helper_(nullptr) {} |
| |
| void OfflinePageTabHelperTest::SetUp() { |
| content::RenderViewHostTestHarness::SetUp(); |
| |
| OfflinePageTabHelper::CreateForWebContents(web_contents()); |
| tab_helper_ = OfflinePageTabHelper::FromWebContents(web_contents()); |
| } |
| |
| void OfflinePageTabHelperTest::TearDown() { |
| content::RenderViewHostTestHarness::TearDown(); |
| } |
| |
| std::unique_ptr<content::BrowserContext> |
| OfflinePageTabHelperTest::CreateBrowserContext() { |
| return TestingProfile::Builder().Build(); |
| } |
| |
| void OfflinePageTabHelperTest::CreateNavigationSimulator(const GURL& url) { |
| navigation_simulator_ = |
| content::NavigationSimulator::CreateBrowserInitiated(url, web_contents()); |
| navigation_simulator_->SetTransition(ui::PAGE_TRANSITION_LINK); |
| } |
| |
| void OfflinePageTabHelperTest::SimulateOfflinePageLoad( |
| const GURL& mhtml_url, |
| base::Time mhtml_creation_time, |
| MHTMLLoadResult load_result) { |
| tab_helper()->SetCurrentTargetFrameForTest( |
| web_contents()->GetPrimaryMainFrame()); |
| |
| // Simulate navigation |
| CreateNavigationSimulator(GURL("file://foo")); |
| navigation_simulator()->Start(); |
| |
| OfflinePageItem offlinePage(mhtml_url, 0, ClientId("async_loading", "1234"), |
| base::FilePath(), 0, mhtml_creation_time); |
| OfflinePageHeader offlineHeader(kTestHeader); |
| tab_helper()->SetOfflinePage( |
| offlinePage, offlineHeader, |
| OfflinePageTrustedState::TRUSTED_AS_IN_INTERNAL_DIR, false); |
| |
| navigation_simulator()->SetContentsMimeType("multipart/related"); |
| |
| tab_helper()->NotifyMhtmlPageLoadAttempted(load_result, mhtml_url, |
| mhtml_creation_time); |
| navigation_simulator()->Commit(); |
| } |
| |
| // Checks the test setup. |
| TEST_F(OfflinePageTabHelperTest, InitialSetup) { |
| CreateNavigationSimulator(GURL("http://mystery.site/foo.html")); |
| EXPECT_NE(nullptr, tab_helper()); |
| } |
| |
| TEST_F(OfflinePageTabHelperTest, MetricsStartNavigation) { |
| CreateNavigationSimulator(GURL("http://mystery.site/foo.html")); |
| // This causes WCO::DidStartNavigation() |
| navigation_simulator()->Start(); |
| } |
| |
| TEST_F(OfflinePageTabHelperTest, MetricsOnlineNavigation) { |
| CreateNavigationSimulator(GURL("http://mystery.site/foo.html")); |
| navigation_simulator()->Start(); |
| navigation_simulator()->Commit(); |
| } |
| |
| TEST_F(OfflinePageTabHelperTest, MetricsOfflineNavigation) { |
| const GURL kTestUrl("http://mystery.site/foo.html"); |
| CreateNavigationSimulator(kTestUrl); |
| navigation_simulator()->Start(); |
| |
| // Simulate offline interceptor loading an offline page instead. |
| OfflinePageItem offlinePage(kTestUrl, 0, ClientId(), base::FilePath(), 0); |
| OfflinePageHeader offlineHeader; |
| tab_helper()->SetOfflinePage( |
| offlinePage, offlineHeader, |
| OfflinePageTrustedState::TRUSTED_AS_IN_INTERNAL_DIR, false); |
| navigation_simulator()->SetContentsMimeType("multipart/related"); |
| |
| navigation_simulator()->Commit(); |
| } |
| |
| TEST_F(OfflinePageTabHelperTest, TrustedInternalOfflinePage) { |
| const GURL kTestUrl("http://mystery.site/foo.html"); |
| CreateNavigationSimulator(kTestUrl); |
| navigation_simulator()->Start(); |
| |
| OfflinePageItem offlinePage(kTestUrl, 0, ClientId(), base::FilePath(), 0); |
| OfflinePageHeader offlineHeader(kTestHeader); |
| tab_helper()->SetOfflinePage( |
| offlinePage, offlineHeader, |
| OfflinePageTrustedState::TRUSTED_AS_IN_INTERNAL_DIR, false); |
| navigation_simulator()->SetContentsMimeType("multipart/related"); |
| navigation_simulator()->Commit(); |
| |
| ASSERT_NE(nullptr, tab_helper()->offline_page()); |
| EXPECT_EQ(kTestUrl, tab_helper()->offline_page()->url); |
| EXPECT_EQ(OfflinePageTrustedState::TRUSTED_AS_IN_INTERNAL_DIR, |
| tab_helper()->trusted_state()); |
| EXPECT_TRUE(tab_helper()->IsShowingTrustedOfflinePage()); |
| EXPECT_EQ(OfflinePageHeader::Reason::DOWNLOAD, |
| tab_helper()->offline_header().reason); |
| } |
| |
| TEST_F(OfflinePageTabHelperTest, TrustedPublicOfflinePage) { |
| const GURL kTestUrl("http://mystery.site/foo.html"); |
| CreateNavigationSimulator(kTestUrl); |
| navigation_simulator()->Start(); |
| |
| OfflinePageItem offlinePage(kTestUrl, 0, ClientId(), base::FilePath(), 0); |
| OfflinePageHeader offlineHeader(kTestHeader); |
| tab_helper()->SetOfflinePage( |
| offlinePage, offlineHeader, |
| OfflinePageTrustedState::TRUSTED_AS_UNMODIFIED_AND_IN_PUBLIC_DIR, false); |
| navigation_simulator()->SetContentsMimeType("multipart/related"); |
| navigation_simulator()->Commit(); |
| |
| ASSERT_NE(nullptr, tab_helper()->offline_page()); |
| EXPECT_EQ(kTestUrl, tab_helper()->offline_page()->url); |
| EXPECT_EQ(OfflinePageTrustedState::TRUSTED_AS_UNMODIFIED_AND_IN_PUBLIC_DIR, |
| tab_helper()->trusted_state()); |
| EXPECT_TRUE(tab_helper()->IsShowingTrustedOfflinePage()); |
| EXPECT_EQ(OfflinePageHeader::Reason::DOWNLOAD, |
| tab_helper()->offline_header().reason); |
| } |
| |
| TEST_F(OfflinePageTabHelperTest, UntrustedOfflinePageForFileUrl) { |
| CreateNavigationSimulator(GURL("file://foo")); |
| navigation_simulator()->Start(); |
| navigation_simulator()->SetContentsMimeType("multipart/related"); |
| navigation_simulator()->Commit(); |
| |
| ASSERT_NE(nullptr, tab_helper()->offline_page()); |
| EXPECT_EQ(OfflinePageTrustedState::UNTRUSTED, tab_helper()->trusted_state()); |
| EXPECT_FALSE(tab_helper()->IsShowingTrustedOfflinePage()); |
| EXPECT_EQ(OfflinePageHeader::Reason::NONE, |
| tab_helper()->offline_header().reason); |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| TEST_F(OfflinePageTabHelperTest, |
| UntrustedOfflinePageForContentUrlWithMultipartRelatedType) { |
| CreateNavigationSimulator(GURL("content://foo")); |
| navigation_simulator()->Start(); |
| navigation_simulator()->SetContentsMimeType("multipart/related"); |
| navigation_simulator()->Commit(); |
| |
| ASSERT_NE(nullptr, tab_helper()->offline_page()); |
| EXPECT_EQ(OfflinePageTrustedState::UNTRUSTED, tab_helper()->trusted_state()); |
| EXPECT_FALSE(tab_helper()->IsShowingTrustedOfflinePage()); |
| EXPECT_EQ(OfflinePageHeader::Reason::NONE, |
| tab_helper()->offline_header().reason); |
| } |
| |
| TEST_F(OfflinePageTabHelperTest, |
| UntrustedOfflinePageForContentUrlWithMessageRfc822Type) { |
| CreateNavigationSimulator(GURL("content://foo")); |
| navigation_simulator()->Start(); |
| navigation_simulator()->SetContentsMimeType("message/rfc822"); |
| navigation_simulator()->Commit(); |
| |
| ASSERT_NE(nullptr, tab_helper()->offline_page()); |
| EXPECT_EQ(OfflinePageTrustedState::UNTRUSTED, tab_helper()->trusted_state()); |
| EXPECT_FALSE(tab_helper()->IsShowingTrustedOfflinePage()); |
| EXPECT_EQ(OfflinePageHeader::Reason::NONE, |
| tab_helper()->offline_header().reason); |
| } |
| #endif |
| |
| TEST_F(OfflinePageTabHelperTest, TestNotifyMhtmlPageLoadAttempted_Success) { |
| GURL mhtml_url("https://www.example.com"); |
| |
| // Simulate navigation |
| SimulateOfflinePageLoad(mhtml_url, kTestMhtmlCreationTime, |
| MHTMLLoadResult::kSuccess); |
| |
| EXPECT_EQ(OfflinePageTrustedState::TRUSTED_AS_IN_INTERNAL_DIR, |
| tab_helper()->trusted_state()); |
| EXPECT_TRUE(tab_helper()->IsShowingTrustedOfflinePage()); |
| EXPECT_EQ(OfflinePageHeader::Reason::DOWNLOAD, |
| tab_helper()->offline_header().reason); |
| |
| const OfflinePageItem* offline_page = tab_helper()->offline_page(); |
| ASSERT_NE(nullptr, offline_page); |
| EXPECT_EQ(mhtml_url, offline_page->url); |
| EXPECT_EQ(kTestMhtmlCreationTime, offline_page->creation_time); |
| } |
| |
| TEST_F(OfflinePageTabHelperTest, |
| TestNotifyMhtmlPageLoadAttempted_BadUrlScheme) { |
| GURL mhtml_url("sftp://www.example.com"); |
| |
| base::HistogramTester histogram_tester; |
| SimulateOfflinePageLoad(mhtml_url, kTestMhtmlCreationTime, |
| MHTMLLoadResult::kUrlSchemeNotAllowed); |
| |
| EXPECT_EQ(OfflinePageTrustedState::TRUSTED_AS_IN_INTERNAL_DIR, |
| tab_helper()->trusted_state()); |
| EXPECT_TRUE(tab_helper()->IsShowingTrustedOfflinePage()); |
| EXPECT_EQ(OfflinePageHeader::Reason::DOWNLOAD, |
| tab_helper()->offline_header().reason); |
| |
| const OfflinePageItem* offline_page = tab_helper()->offline_page(); |
| EXPECT_EQ(mhtml_url, offline_page->url); |
| EXPECT_EQ(kTestMhtmlCreationTime, offline_page->creation_time); |
| } |
| |
| TEST_F(OfflinePageTabHelperTest, TestNotifyMhtmlPageLoadAttempted_Untrusted) { |
| GURL mhtml_url("https://www.example.com"); |
| |
| tab_helper()->SetCurrentTargetFrameForTest( |
| web_contents()->GetPrimaryMainFrame()); |
| |
| // Simulate navigation |
| CreateNavigationSimulator(GURL("file://foo")); |
| navigation_simulator()->Start(); |
| |
| // We force use of the untrusted page histogram by using an empty namespace. |
| OfflinePageItem offlinePage(mhtml_url, 0, ClientId("", "1234"), |
| base::FilePath(), 0, kTestMhtmlCreationTime); |
| OfflinePageHeader offlineHeader(kTestHeader); |
| tab_helper()->SetOfflinePage(offlinePage, offlineHeader, |
| OfflinePageTrustedState::UNTRUSTED, false); |
| |
| navigation_simulator()->SetContentsMimeType("multipart/related"); |
| |
| tab_helper()->NotifyMhtmlPageLoadAttempted(MHTMLLoadResult::kSuccess, |
| mhtml_url, kTestMhtmlCreationTime); |
| navigation_simulator()->Commit(); |
| } |
| |
| TEST_F(OfflinePageTabHelperTest, AbortedNavigationDoesNotResetOfflineInfo) { |
| GURL mhtml_url("https://www.example.com"); |
| SimulateOfflinePageLoad(mhtml_url, kTestMhtmlCreationTime, |
| MHTMLLoadResult::kUrlSchemeNotAllowed); |
| auto navigation = content::NavigationSimulator::CreateBrowserInitiated( |
| GURL("http://mystery.site/foo.html"), web_contents()); |
| navigation->Start(); |
| navigation->AbortCommit(); |
| EXPECT_TRUE(tab_helper()->offline_page()); |
| } |
| |
| TEST_F(OfflinePageTabHelperTest, OfflinePageIsNotStoredInBackForwardCache) { |
| content::BackForwardCacheDisabledTester back_forward_cache_tester; |
| |
| const GURL kTestUrl("http://mystery.site/foo.html"); |
| CreateNavigationSimulator(kTestUrl); |
| navigation_simulator()->Start(); |
| |
| SimulateOfflinePageLoad(kTestUrl, kTestMhtmlCreationTime, |
| MHTMLLoadResult::kSuccess); |
| |
| int process_id = web_contents()->GetPrimaryMainFrame()->GetProcess()->GetID(); |
| int main_frame_id = web_contents()->GetPrimaryMainFrame()->GetRoutingID(); |
| |
| // Navigate away. |
| content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(), |
| kTestUrl); |
| EXPECT_TRUE(back_forward_cache_tester.IsDisabledForFrameWithReason( |
| process_id, main_frame_id, |
| back_forward_cache::DisabledReason( |
| back_forward_cache::DisabledReasonId::kOfflinePage))); |
| } |
| |
| class OfflinePageTabHelperFencedFrameTest : public OfflinePageTabHelperTest { |
| public: |
| OfflinePageTabHelperFencedFrameTest() { |
| scoped_feature_list_.InitAndEnableFeatureWithParameters( |
| blink::features::kFencedFrames, {{"implementation_type", "mparch"}}); |
| } |
| ~OfflinePageTabHelperFencedFrameTest() override = default; |
| |
| content::RenderFrameHost* CreateFencedFrame( |
| content::RenderFrameHost* parent) { |
| content::RenderFrameHost* fenced_frame = |
| content::RenderFrameHostTester::For(parent)->AppendFencedFrame(); |
| return fenced_frame; |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| TEST_F(OfflinePageTabHelperFencedFrameTest, DoNotRecordMetricsInFencedFrame) { |
| const GURL kTestUrl("http://mystery.site/foo.html"); |
| CreateNavigationSimulator(kTestUrl); |
| navigation_simulator()->Start(); |
| |
| // Simulate offline interceptor loading an offline page instead. |
| OfflinePageItem offlinePage(kTestUrl, 0, ClientId(), base::FilePath(), 0); |
| OfflinePageHeader offlineHeader; |
| tab_helper()->SetOfflinePage( |
| offlinePage, offlineHeader, |
| OfflinePageTrustedState::TRUSTED_AS_IN_INTERNAL_DIR, |
| true /* is_offline_preview */); |
| navigation_simulator()->SetContentsMimeType("multipart/related"); |
| navigation_simulator()->Commit(); |
| |
| // Ensure that the offline page exists via an offline preview item. |
| const OfflinePageItem* offline_page_item = |
| tab_helper()->GetOfflinePreviewItem(); |
| EXPECT_NE(offline_page_item, nullptr); |
| |
| // Create a fenced frame. |
| content::RenderFrameHostTester::For(main_rfh()) |
| ->InitializeRenderFrameIfNeeded(); |
| content::RenderFrameHost* fenced_frame_rfh = CreateFencedFrame(main_rfh()); |
| GURL kFencedFrameUrl("https://fencedframe.com"); |
| std::unique_ptr<content::NavigationSimulator> navigation_simulator = |
| content::NavigationSimulator::CreateRendererInitiated(kFencedFrameUrl, |
| fenced_frame_rfh); |
| navigation_simulator->Commit(); |
| EXPECT_TRUE(fenced_frame_rfh->IsFencedFrameRoot()); |
| |
| // The offline preview item should not be cleared by the fenced frame's |
| // navigation and should be same as |offline_page_item|. |
| EXPECT_EQ(tab_helper()->GetOfflinePreviewItem(), offline_page_item); |
| } |
| |
| } // namespace |
| } // namespace offline_pages |