blob: 86fa47f944f59582524b02dc1181db559487bacd [file] [log] [blame]
// Copyright 2015 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 "third_party/blink/renderer/core/loader/document_loader.h"
#include <queue>
#include <string>
#include <utility>
#include "base/auto_reset.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/frame/frame_owner_element_type.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/public/platform/web_url_loader_client.h"
#include "third_party/blink/public/platform/web_url_loader_mock_factory.h"
#include "third_party/blink/renderer/core/frame/frame_test_helpers.h"
#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/testing/scoped_fake_plugin_registry.h"
#include "third_party/blink/renderer/platform/loader/static_data_navigation_body_loader.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
#include "third_party/blink/renderer/platform/testing/url_test_helpers.h"
namespace blink {
// TODO(dcheng): Ideally, enough of frame_test_helpers would be in core/ that
// placing a test for a core/ class in web/ wouldn't be necessary.
class DocumentLoaderTest : public testing::Test {
protected:
void SetUp() override {
web_view_helper_.Initialize();
url_test_helpers::RegisterMockedURLLoad(
url_test_helpers::ToKURL("https://example.com/foo.html"),
test::CoreTestDataPath("foo.html"));
}
void TearDown() override {
Platform::Current()
->GetURLLoaderMockFactory()
->UnregisterAllURLsAndClearMemoryCache();
}
WebLocalFrameImpl* MainFrame() { return web_view_helper_.LocalMainFrame(); }
frame_test_helpers::WebViewHelper web_view_helper_;
};
TEST_F(DocumentLoaderTest, SingleChunk) {
class TestDelegate : public WebURLLoaderTestDelegate {
public:
void DidReceiveData(WebURLLoaderClient* original_client,
const char* data,
int data_length) override {
EXPECT_EQ(34, data_length) << "foo.html was not served in a single chunk";
original_client->DidReceiveData(data, data_length);
}
} delegate;
Platform::Current()->GetURLLoaderMockFactory()->SetLoaderDelegate(&delegate);
frame_test_helpers::LoadFrame(MainFrame(), "https://example.com/foo.html");
Platform::Current()->GetURLLoaderMockFactory()->SetLoaderDelegate(nullptr);
// TODO(dcheng): How should the test verify that the original callback is
// invoked? The test currently still passes even if the test delegate
// forgets to invoke the callback.
}
// Test normal case of DocumentLoader::dataReceived(): data in multiple chunks,
// with no reentrancy.
TEST_F(DocumentLoaderTest, MultiChunkNoReentrancy) {
class TestDelegate : public WebURLLoaderTestDelegate {
public:
void DidReceiveData(WebURLLoaderClient* original_client,
const char* data,
int data_length) override {
EXPECT_EQ(34, data_length) << "foo.html was not served in a single chunk";
// Chunk the reply into one byte chunks.
for (int i = 0; i < data_length; ++i)
original_client->DidReceiveData(&data[i], 1);
}
} delegate;
Platform::Current()->GetURLLoaderMockFactory()->SetLoaderDelegate(&delegate);
frame_test_helpers::LoadFrame(MainFrame(), "https://example.com/foo.html");
Platform::Current()->GetURLLoaderMockFactory()->SetLoaderDelegate(nullptr);
}
// Finally, test reentrant callbacks to DocumentLoader::BodyDataReceived().
TEST_F(DocumentLoaderTest, MultiChunkWithReentrancy) {
// This test delegate chunks the response stage into three distinct stages:
// 1. The first BodyDataReceived() callback, which triggers frame detach
// due to committing a provisional load.
// 2. The middle part of the response, which is dispatched to
// BodyDataReceived() reentrantly.
// 3. The final chunk, which is dispatched normally at the top-level.
class MainFrameClient : public WebURLLoaderTestDelegate,
public frame_test_helpers::TestWebFrameClient {
public:
// WebURLLoaderTestDelegate overrides:
bool FillNavigationParamsResponse(WebNavigationParams* params) override {
params->response = WebURLResponse(params->url);
params->response.SetMimeType("application/pdf");
params->response.SetHttpStatusCode(200);
std::string data("<html><body>foo</body></html>");
for (size_t i = 0; i < data.size(); i++)
data_.push(data[i]);
auto body_loader = std::make_unique<StaticDataNavigationBodyLoader>();
body_loader_ = body_loader.get();
params->body_loader = std::move(body_loader);
return true;
}
void Serve() {
{
// Serve the first byte to the real WebURLLoaderCLient, which
// should trigger frameDetach() due to committing a provisional
// load.
base::AutoReset<bool> dispatching(&dispatching_did_receive_data_, true);
DispatchOneByte();
}
// Serve the remaining bytes to complete the load.
EXPECT_FALSE(data_.empty());
while (!data_.empty())
DispatchOneByte();
body_loader_->Finish();
body_loader_ = nullptr;
}
// WebLocalFrameClient overrides:
void RunScriptsAtDocumentElementAvailable() override {
if (dispatching_did_receive_data_) {
// This should be called by the first BodyDataReceived() call, since
// it should create a plugin document structure and trigger this.
EXPECT_GT(data_.size(), 10u);
// Dispatch BodyDataReceived() callbacks for part of the remaining
// data, saving the rest to be dispatched at the top-level as
// normal.
while (data_.size() > 10)
DispatchOneByte();
served_reentrantly_ = true;
}
TestWebFrameClient::RunScriptsAtDocumentElementAvailable();
}
void DispatchOneByte() {
char c = data_.front();
data_.pop();
body_loader_->Write(&c, 1);
}
bool ServedReentrantly() const { return served_reentrantly_; }
private:
std::queue<char> data_;
bool dispatching_did_receive_data_ = false;
bool served_reentrantly_ = false;
StaticDataNavigationBodyLoader* body_loader_ = nullptr;
};
// We use a plugin document triggered by "application/pdf" mime type,
// because that gives us reliable way to get a WebLocalFrameClient callback
// from inside BodyDataReceived() call.
ScopedFakePluginRegistry fake_plugins;
MainFrameClient main_frame_client;
web_view_helper_.Initialize(&main_frame_client);
web_view_helper_.GetWebView()->GetPage()->GetSettings().SetPluginsEnabled(
true);
Platform::Current()->GetURLLoaderMockFactory()->SetLoaderDelegate(
&main_frame_client);
frame_test_helpers::LoadFrameDontWait(
MainFrame(), url_test_helpers::ToKURL("https://example.com/foo.html"));
main_frame_client.Serve();
frame_test_helpers::PumpPendingRequestsForFrameToLoad(MainFrame());
Platform::Current()->GetURLLoaderMockFactory()->SetLoaderDelegate(nullptr);
// Sanity check that we did actually test reeentrancy.
EXPECT_TRUE(main_frame_client.ServedReentrantly());
// MainFrameClient is stack-allocated, so manually Reset to avoid UAF.
web_view_helper_.Reset();
}
TEST_F(DocumentLoaderTest, isCommittedButEmpty) {
WebViewImpl* web_view_impl =
web_view_helper_.InitializeAndLoad("about:blank");
EXPECT_TRUE(To<LocalFrame>(web_view_impl->GetPage()->MainFrame())
->Loader()
.GetDocumentLoader()
->IsCommittedButEmpty());
}
TEST_F(DocumentLoaderTest, MixedContentOptOutSetIfHeaderReceived) {
WebURL url =
url_test_helpers::ToKURL("https://examplenoupgrade.com/foo.html");
WebURLResponse response(url);
response.SetHttpStatusCode(200);
response.SetHttpHeaderField("mixed-content", "noupgrade");
response.SetMimeType("text/html");
url_test_helpers::RegisterMockedURLLoadWithCustomResponse(
url, test::CoreTestDataPath("foo.html"), response);
WebViewImpl* web_view_impl = web_view_helper_.InitializeAndLoad(
"https://examplenoupgrade.com/foo.html");
EXPECT_TRUE(To<LocalFrame>(web_view_impl->GetPage()->MainFrame())
->GetDocument()
->GetMixedAutoUpgradeOptOut());
}
TEST_F(DocumentLoaderTest, MixedContentOptOutNotSetIfNoHeaderReceived) {
WebViewImpl* web_view_impl =
web_view_helper_.InitializeAndLoad("https://example.com/foo.html");
EXPECT_FALSE(To<LocalFrame>(web_view_impl->GetPage()->MainFrame())
->Loader()
.GetDocumentLoader()
->GetFrame()
->GetDocument()
->GetMixedAutoUpgradeOptOut());
}
} // namespace blink