| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "extensions/renderer/extension_localization_throttle.h" |
| |
| #include <string_view> |
| |
| #include "base/json/json_reader.h" |
| #include "base/test/task_environment.h" |
| #include "content/public/test/mock_render_process_host.h" |
| #include "content/public/test/mock_render_thread.h" |
| #include "extensions/common/extension_builder.h" |
| #include "extensions/renderer/renderer_extension_registry.h" |
| #include "extensions/renderer/shared_l10n_map.h" |
| #include "extensions/renderer/test_extensions_renderer_client.h" |
| #include "mojo/public/cpp/bindings/receiver.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "mojo/public/cpp/system/data_pipe_utils.h" |
| #include "mojo/public/cpp/system/string_data_source.h" |
| #include "net/base/request_priority.h" |
| #include "services/network/test/test_url_loader_client.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/loader/url_loader_throttle.h" |
| #include "third_party/blink/public/platform/web_url.h" |
| |
| namespace extensions { |
| namespace { |
| |
| class FakeURLLoader final : public network::mojom::URLLoader { |
| public: |
| explicit FakeURLLoader( |
| mojo::PendingReceiver<network::mojom::URLLoader> url_loader_receiver) |
| : receiver_(this, std::move(url_loader_receiver)) {} |
| ~FakeURLLoader() override = default; |
| |
| FakeURLLoader(const FakeURLLoader&) = delete; |
| FakeURLLoader& operator=(const FakeURLLoader&) = delete; |
| |
| // network::mojom::URLLoader overrides. |
| void FollowRedirect( |
| const std::vector<std::string>& removed_headers, |
| const net::HttpRequestHeaders& modified_headers, |
| const net::HttpRequestHeaders& modified_cors_exempt_headers, |
| const std::optional<GURL>& new_url) override { |
| NOTREACHED(); |
| } |
| void SetPriority(net::RequestPriority priority, |
| int32_t intra_priority_value) override { |
| set_priority_called_ = true; |
| } |
| |
| bool set_priority_called() const { return set_priority_called_; } |
| |
| private: |
| bool set_priority_called_ = false; |
| |
| mojo::Receiver<network::mojom::URLLoader> receiver_; |
| }; |
| |
| class FakeDelegate : public blink::URLLoaderThrottle::Delegate { |
| public: |
| // Implements blink::URLLoaderThrottle::Delegate. |
| void CancelWithError(int error_code, |
| std::string_view custom_reason) override { |
| cancel_error_code_ = error_code; |
| cancel_custom_reason_ = std::string(custom_reason); |
| } |
| void Resume() override { NOTREACHED(); } |
| |
| void UpdateDeferredResponseHead( |
| network::mojom::URLResponseHeadPtr new_response_head, |
| mojo::ScopedDataPipeConsumerHandle body) override { |
| NOTREACHED(); |
| } |
| void InterceptResponse( |
| mojo::PendingRemote<network::mojom::URLLoader> new_loader, |
| mojo::PendingReceiver<network::mojom::URLLoaderClient> |
| new_client_receiver, |
| mojo::PendingRemote<network::mojom::URLLoader>* original_loader, |
| mojo::PendingReceiver<network::mojom::URLLoaderClient>* |
| original_client_receiver, |
| mojo::ScopedDataPipeConsumerHandle* body) override { |
| is_intercepted_ = true; |
| |
| destination_loader_remote_.Bind(std::move(new_loader)); |
| ASSERT_TRUE( |
| mojo::FusePipes(std::move(new_client_receiver), |
| mojo::PendingRemote<network::mojom::URLLoaderClient>( |
| destination_loader_client_.CreateRemote()))); |
| source_url_loader_ = std::make_unique<FakeURLLoader>( |
| original_loader->InitWithNewPipeAndPassReceiver()); |
| |
| *original_client_receiver = |
| source_loader_client_remote_.BindNewPipeAndPassReceiver(); |
| |
| DCHECK(!source_body_handle_); |
| |
| mojo::ScopedDataPipeConsumerHandle consumer_handle; |
| EXPECT_EQ(MOJO_RESULT_OK, |
| mojo::CreateDataPipe(/*options=*/nullptr, source_body_handle_, |
| consumer_handle)); |
| body->swap(consumer_handle); |
| |
| destination_loader_client()->OnReceiveResponse( |
| network::mojom::URLResponseHead::New(), std::move(consumer_handle), |
| std::nullopt); |
| } |
| |
| void LoadResponseBody(const std::string& body) { |
| mojo::BlockingCopyFromString(body, source_body_handle_); |
| } |
| |
| void CompleteResponse() { |
| source_loader_client_remote()->OnComplete( |
| network::URLLoaderCompletionStatus()); |
| source_body_handle_.reset(); |
| } |
| |
| bool is_intercepted() const { return is_intercepted_; } |
| const std::optional<int>& cancel_error_code() const { |
| return cancel_error_code_; |
| } |
| const std::optional<std::string>& cancel_custom_reason() const { |
| return cancel_custom_reason_; |
| } |
| |
| mojo::Remote<network::mojom::URLLoader>& destination_loader_remote() { |
| return destination_loader_remote_; |
| } |
| |
| network::TestURLLoaderClient* destination_loader_client() { |
| return &destination_loader_client_; |
| } |
| |
| FakeURLLoader* source_url_loader() { return source_url_loader_.get(); } |
| |
| mojo::Remote<network::mojom::URLLoaderClient>& source_loader_client_remote() { |
| return source_loader_client_remote_; |
| } |
| |
| mojo::ScopedDataPipeProducerHandle& source_body_handle() { |
| return source_body_handle_; |
| } |
| |
| private: |
| bool is_intercepted_ = false; |
| std::optional<int> cancel_error_code_; |
| std::optional<std::string> cancel_custom_reason_; |
| |
| // The chain of mojom::URLLoaderClient: |
| // [Blink side] |
| // destination_loader_client_ |
| // <- ExtensionLocalizationURLLoader::destination_url_loader_client_ |
| // <- ExtensionLocalizationURLLoader |
| // <- ExtensionLocalizationURLLoader::source_url_client_receiver_ |
| // <- source_loader_client_remote_ |
| // [Browser process side] |
| |
| // The chain of mojom::URLLoader: |
| // [Blink side] |
| // destination_loader_remote_ |
| // -> ExtensionLocalizationURLLoader (SelfOwnedReceiver) |
| // -> ExtensionLocalizationURLLoader::source_url_loader_ |
| // -> source_url_loader_ |
| // [Browser process side] |
| |
| mojo::Remote<network::mojom::URLLoader> destination_loader_remote_; |
| network::TestURLLoaderClient destination_loader_client_; |
| |
| std::unique_ptr<FakeURLLoader> source_url_loader_; |
| mojo::Remote<network::mojom::URLLoaderClient> source_loader_client_remote_; |
| |
| mojo::ScopedDataPipeProducerHandle source_body_handle_; |
| }; |
| |
| class ExtensionLocalizationThrottleTest : public testing::Test { |
| protected: |
| void SetUp() override { |
| extensions::SharedL10nMap::L10nMessagesMap messages; |
| messages.insert(std::make_pair("hello", "hola")); |
| messages.insert(std::make_pair("world", "mundo")); |
| extensions::SharedL10nMap::GetInstance().SetMessagesForTesting( |
| "aabbccddeeffgghhiijjkkllmmnnoopp", std::move(messages)); |
| } |
| // Be the first member so it is destroyed last. |
| base::test::TaskEnvironment task_environment_; |
| |
| // A gurl with a valid extension id. |
| GURL test_gurl_ = GURL("chrome-extension://aabbccddeeffgghhiijjkkllmmnnoopp"); |
| }; |
| |
| TEST_F(ExtensionLocalizationThrottleTest, DoNotCreate) { |
| EXPECT_FALSE(ExtensionLocalizationThrottle::MaybeCreate( |
| std::nullopt, blink::WebURL(GURL("https://example.com/test.css")))); |
| EXPECT_FALSE(ExtensionLocalizationThrottle::MaybeCreate( |
| std::nullopt, blink::WebURL(GURL("http://example.com/test.css")))); |
| } |
| |
| TEST_F(ExtensionLocalizationThrottleTest, DoNotIntercept) { |
| const GURL url = test_gurl_.Resolve("test.txt"); |
| auto throttle = ExtensionLocalizationThrottle::MaybeCreate( |
| std::nullopt, blink::WebURL(url)); |
| ASSERT_TRUE(throttle); |
| auto delegate = std::make_unique<FakeDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| auto response_head = network::mojom::URLResponseHead::New(); |
| response_head->mime_type = "text/plain"; |
| bool defer = false; |
| throttle->WillProcessResponse(url, response_head.get(), &defer); |
| EXPECT_FALSE(defer); |
| EXPECT_FALSE(delegate->is_intercepted()); |
| } |
| |
| TEST_F(ExtensionLocalizationThrottleTest, OneMessage) { |
| const GURL url = test_gurl_.Resolve("test.css"); |
| auto throttle = ExtensionLocalizationThrottle::MaybeCreate( |
| std::nullopt, blink::WebURL(url)); |
| ASSERT_TRUE(throttle); |
| |
| auto delegate = std::make_unique<FakeDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| auto response_head = network::mojom::URLResponseHead::New(); |
| response_head->mime_type = "text/css"; |
| bool defer = false; |
| throttle->WillProcessResponse(url, response_head.get(), &defer); |
| EXPECT_FALSE(defer); |
| EXPECT_TRUE(delegate->is_intercepted()); |
| delegate->LoadResponseBody("__MSG_hello__!"); |
| delegate->CompleteResponse(); |
| delegate->destination_loader_client()->RunUntilComplete(); |
| |
| std::string response; |
| EXPECT_TRUE(mojo::BlockingCopyToString( |
| delegate->destination_loader_client()->response_body_release(), |
| &response)); |
| EXPECT_EQ("hola!", response); |
| EXPECT_EQ( |
| net::OK, |
| delegate->destination_loader_client()->completion_status().error_code); |
| } |
| |
| TEST_F(ExtensionLocalizationThrottleTest, TwoMessages) { |
| const GURL url = test_gurl_.Resolve("test.css"); |
| auto throttle = ExtensionLocalizationThrottle::MaybeCreate( |
| std::nullopt, blink::WebURL(url)); |
| ASSERT_TRUE(throttle); |
| |
| auto delegate = std::make_unique<FakeDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| auto response_head = network::mojom::URLResponseHead::New(); |
| response_head->mime_type = "text/css"; |
| bool defer = false; |
| throttle->WillProcessResponse(url, response_head.get(), &defer); |
| EXPECT_FALSE(defer); |
| EXPECT_TRUE(delegate->is_intercepted()); |
| delegate->LoadResponseBody("__MSG_hello__ __MSG"); |
| task_environment_.RunUntilIdle(); |
| delegate->LoadResponseBody("_world__!"); |
| delegate->CompleteResponse(); |
| |
| delegate->destination_loader_client()->RunUntilComplete(); |
| |
| std::string response; |
| EXPECT_TRUE(mojo::BlockingCopyToString( |
| delegate->destination_loader_client()->response_body_release(), |
| &response)); |
| EXPECT_EQ("hola mundo!", response); |
| EXPECT_EQ( |
| net::OK, |
| delegate->destination_loader_client()->completion_status().error_code); |
| } |
| |
| TEST_F(ExtensionLocalizationThrottleTest, EmptyData) { |
| const GURL url = test_gurl_.Resolve("test.css"); |
| auto throttle = ExtensionLocalizationThrottle::MaybeCreate( |
| std::nullopt, blink::WebURL(url)); |
| ASSERT_TRUE(throttle); |
| |
| auto delegate = std::make_unique<FakeDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| auto response_head = network::mojom::URLResponseHead::New(); |
| response_head->mime_type = "text/css"; |
| bool defer = false; |
| throttle->WillProcessResponse(url, response_head.get(), &defer); |
| EXPECT_FALSE(defer); |
| EXPECT_TRUE(delegate->is_intercepted()); |
| delegate->CompleteResponse(); |
| delegate->destination_loader_client()->RunUntilComplete(); |
| |
| std::string response; |
| EXPECT_TRUE(mojo::BlockingCopyToString( |
| delegate->destination_loader_client()->response_body_release(), |
| &response)); |
| EXPECT_EQ("", response); |
| EXPECT_EQ( |
| net::OK, |
| delegate->destination_loader_client()->completion_status().error_code); |
| } |
| |
| // Regression test for https://crbug.com/1475798 |
| TEST_F(ExtensionLocalizationThrottleTest, Cancel) { |
| const GURL url = test_gurl_.Resolve("test.css"); |
| auto throttle = ExtensionLocalizationThrottle::MaybeCreate( |
| std::nullopt, blink::WebURL(url)); |
| ASSERT_TRUE(throttle); |
| |
| auto delegate = std::make_unique<FakeDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| auto response_head = network::mojom::URLResponseHead::New(); |
| response_head->mime_type = "text/css"; |
| bool defer = false; |
| throttle->WillProcessResponse(url, response_head.get(), &defer); |
| EXPECT_FALSE(defer); |
| EXPECT_TRUE(delegate->is_intercepted()); |
| delegate->LoadResponseBody("__MSG_hello__!"); |
| delegate->CompleteResponse(); |
| // Run all tasks in the main thread to make DataPipeProducer::SequenceState |
| // call PostTask(&SequenceState::StartOnSequence) to a background thread. |
| base::RunLoop().RunUntilIdle(); |
| // Resetting `destination_loader_remote` triggers |
| // ExtensionLocalizationURLLoader destruction. |
| delegate->destination_loader_remote().reset(); |
| // Run all tasks in the main thread to destroy the |
| // ExtensionLocalizationURLLoader. |
| base::RunLoop().RunUntilIdle(); |
| // Runs SequenceState::StartOnSequence in the background thread. |
| task_environment_.RunUntilIdle(); |
| } |
| |
| TEST_F(ExtensionLocalizationThrottleTest, SourceSideError) { |
| const GURL url = test_gurl_.Resolve("test.css"); |
| auto throttle = ExtensionLocalizationThrottle::MaybeCreate( |
| std::nullopt, blink::WebURL(url)); |
| ASSERT_TRUE(throttle); |
| |
| auto delegate = std::make_unique<FakeDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| auto response_head = network::mojom::URLResponseHead::New(); |
| response_head->mime_type = "text/css"; |
| bool defer = false; |
| throttle->WillProcessResponse(url, response_head.get(), &defer); |
| EXPECT_FALSE(defer); |
| EXPECT_TRUE(delegate->is_intercepted()); |
| delegate->LoadResponseBody("__MSG_hello__!"); |
| |
| delegate->source_loader_client_remote()->OnComplete( |
| network::URLLoaderCompletionStatus(net::ERR_OUT_OF_MEMORY)); |
| delegate->source_body_handle().reset(); |
| |
| delegate->destination_loader_client()->RunUntilComplete(); |
| |
| std::string response; |
| EXPECT_TRUE(mojo::BlockingCopyToString( |
| delegate->destination_loader_client()->response_body_release(), |
| &response)); |
| EXPECT_EQ("hola!", response); |
| EXPECT_EQ( |
| net::ERR_OUT_OF_MEMORY, |
| delegate->destination_loader_client()->completion_status().error_code); |
| } |
| |
| TEST_F(ExtensionLocalizationThrottleTest, WriteError) { |
| const GURL url = test_gurl_.Resolve("test.css"); |
| auto throttle = ExtensionLocalizationThrottle::MaybeCreate( |
| std::nullopt, blink::WebURL(url)); |
| ASSERT_TRUE(throttle); |
| |
| auto delegate = std::make_unique<FakeDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| auto response_head = network::mojom::URLResponseHead::New(); |
| response_head->mime_type = "text/css"; |
| bool defer = false; |
| throttle->WillProcessResponse(url, response_head.get(), &defer); |
| EXPECT_FALSE(defer); |
| EXPECT_TRUE(delegate->is_intercepted()); |
| |
| // Release the body to cause write error. |
| delegate->destination_loader_client()->response_body_release(); |
| task_environment_.RunUntilIdle(); |
| |
| delegate->LoadResponseBody("__MSG_hello__!"); |
| delegate->CompleteResponse(); |
| delegate->destination_loader_client()->RunUntilComplete(); |
| |
| EXPECT_EQ( |
| net::ERR_INSUFFICIENT_RESOURCES, |
| delegate->destination_loader_client()->completion_status().error_code); |
| } |
| |
| TEST_F(ExtensionLocalizationThrottleTest, CreateDataPipeError) { |
| const GURL url = test_gurl_.Resolve("test.css"); |
| auto throttle = ExtensionLocalizationThrottle::MaybeCreate( |
| std::nullopt, blink::WebURL(url)); |
| ASSERT_TRUE(throttle); |
| throttle->ForceCreateDataPipeErrorForTest(); |
| |
| auto delegate = std::make_unique<FakeDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| auto response_head = network::mojom::URLResponseHead::New(); |
| response_head->mime_type = "text/css"; |
| bool defer = false; |
| throttle->WillProcessResponse(url, response_head.get(), &defer); |
| EXPECT_TRUE(defer); |
| EXPECT_FALSE(delegate->is_intercepted()); |
| EXPECT_FALSE(delegate->cancel_error_code()); |
| |
| // Run loop to call DeferredCancelWithError(). |
| base::RunLoop().RunUntilIdle(); |
| |
| ASSERT_TRUE(delegate->cancel_error_code()); |
| EXPECT_EQ(net::ERR_INSUFFICIENT_RESOURCES, *delegate->cancel_error_code()); |
| ASSERT_TRUE(delegate->cancel_custom_reason()); |
| EXPECT_EQ("ExtensionLocalizationThrottle", *delegate->cancel_custom_reason()); |
| } |
| |
| TEST_F(ExtensionLocalizationThrottleTest, URLLoaderChain) { |
| const GURL url = test_gurl_.Resolve("test.css"); |
| auto throttle = ExtensionLocalizationThrottle::MaybeCreate( |
| std::nullopt, blink::WebURL(url)); |
| ASSERT_TRUE(throttle); |
| |
| auto delegate = std::make_unique<FakeDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| auto response_head = network::mojom::URLResponseHead::New(); |
| response_head->mime_type = "text/css"; |
| bool defer = false; |
| throttle->WillProcessResponse(url, response_head.get(), &defer); |
| EXPECT_FALSE(defer); |
| EXPECT_TRUE(delegate->is_intercepted()); |
| |
| FakeURLLoader* source_url_loader = delegate->source_url_loader(); |
| mojo::Remote<network::mojom::URLLoader>& destination_loader_remote = |
| delegate->destination_loader_remote(); |
| |
| ASSERT_TRUE(source_url_loader); |
| EXPECT_FALSE(source_url_loader->set_priority_called()); |
| |
| destination_loader_remote->SetPriority(net::LOW, 1); |
| task_environment_.RunUntilIdle(); |
| EXPECT_TRUE(source_url_loader->set_priority_called()); |
| |
| delegate->LoadResponseBody("__MSG_hello__!"); |
| delegate->CompleteResponse(); |
| delegate->destination_loader_client()->RunUntilComplete(); |
| |
| std::string response; |
| EXPECT_TRUE(mojo::BlockingCopyToString( |
| delegate->destination_loader_client()->response_body_release(), |
| &response)); |
| EXPECT_EQ("hola!", response); |
| EXPECT_EQ( |
| net::OK, |
| delegate->destination_loader_client()->completion_status().error_code); |
| } |
| |
| TEST_F(ExtensionLocalizationThrottleTest, |
| URLLoaderClientOnTransferSizeUpdated) { |
| const GURL url = test_gurl_.Resolve("test.css"); |
| auto throttle = ExtensionLocalizationThrottle::MaybeCreate( |
| std::nullopt, blink::WebURL(url)); |
| ASSERT_TRUE(throttle); |
| |
| auto delegate = std::make_unique<FakeDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| auto response_head = network::mojom::URLResponseHead::New(); |
| response_head->mime_type = "text/css"; |
| bool defer = false; |
| throttle->WillProcessResponse(url, response_head.get(), &defer); |
| EXPECT_FALSE(defer); |
| EXPECT_TRUE(delegate->is_intercepted()); |
| |
| network::TestURLLoaderClient* destination_loader_client = |
| delegate->destination_loader_client(); |
| mojo::Remote<network::mojom::URLLoaderClient>& source_loader_client_remote = |
| delegate->source_loader_client_remote(); |
| |
| ASSERT_TRUE(destination_loader_client); |
| EXPECT_EQ(0, destination_loader_client->body_transfer_size()); |
| |
| source_loader_client_remote->OnTransferSizeUpdated(/*transfer_size_diff=*/10); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ(10, destination_loader_client->body_transfer_size()); |
| |
| delegate->LoadResponseBody("__MSG_hello__!"); |
| delegate->CompleteResponse(); |
| destination_loader_client->RunUntilComplete(); |
| |
| std::string response; |
| EXPECT_TRUE(mojo::BlockingCopyToString( |
| destination_loader_client->response_body_release(), &response)); |
| EXPECT_EQ("hola!", response); |
| EXPECT_EQ(net::OK, destination_loader_client->completion_status().error_code); |
| } |
| |
| // A renderer thread is required to be able to use RendererExtensionRegistry. |
| class ExtensionLocalizationThrottleTestWithRendererThread |
| : public ExtensionLocalizationThrottleTest { |
| public: |
| void SetUp() override { |
| render_thread_ = std::make_unique<content::MockRenderThread>(); |
| renderer_client_ = std::make_unique<TestExtensionsRendererClient>(); |
| ExtensionsRendererClient::Set(renderer_client_.get()); |
| } |
| |
| protected: |
| // Return an extension when provided with a valid json manifest. |
| scoped_refptr<const Extension> GetExtension( |
| const std::string& manifest_json) { |
| std::u16string error; |
| base::Value::Dict manifest_dict; |
| auto manifest_value = |
| base::JSONReader::ReadDict(manifest_json, base::JSON_PARSE_RFC); |
| EXPECT_TRUE(manifest_value.has_value()); |
| manifest_dict = std::move(*manifest_value); |
| scoped_refptr<const Extension> extension = Extension::Create( |
| base::FilePath(), extensions::mojom::ManifestLocation::kInternal, |
| manifest_dict, Extension::NO_FLAGS, &error); |
| EXPECT_TRUE(extension) << error; |
| return extension; |
| } |
| |
| private: |
| std::unique_ptr<content::MockRenderThread> render_thread_; |
| std::unique_ptr<ExtensionsRendererClient> renderer_client_; |
| }; |
| |
| // Ensure that extension ids are used instead of guids in very rare scenarios. |
| TEST_F(ExtensionLocalizationThrottleTestWithRendererThread, |
| ExtensionIdInsteadOfGuid) { |
| std::string manifest_json = R"({ |
| "name": "Test", |
| "version": "1.0", |
| "manifest_version": 3, |
| "resources": [{ |
| "resources": ["styles.css"], |
| "matches": ["https://allowed.example/*"], |
| "use_dynamic_url": true |
| }] |
| })"; |
| |
| auto extension = GetExtension(manifest_json); |
| ASSERT_TRUE(extension); |
| |
| RendererExtensionRegistry::Get()->Insert(extension); |
| |
| auto process_response = [](const GURL& gurl) { |
| auto throttle = ExtensionLocalizationThrottle::MaybeCreate( |
| std::nullopt, blink::WebURL(gurl)); |
| ASSERT_TRUE(throttle); |
| auto delegate = std::make_unique<FakeDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| auto response_head = network::mojom::URLResponseHead::New(); |
| response_head->mime_type = "text/css"; |
| bool defer = false; |
| throttle->WillProcessResponse(gurl, response_head.get(), &defer); |
| EXPECT_FALSE(defer); |
| EXPECT_TRUE(delegate->is_intercepted()); |
| }; |
| |
| process_response(extension->GetResourceURL("styles.css")); |
| process_response(extension->dynamic_url().Resolve("styles.css")); |
| } |
| |
| } // namespace |
| } // namespace extensions |