| // Copyright (c) 2012 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 <list> |
| #include <set> |
| |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/macros.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "build/build_config.h" |
| #include "chrome/app/chrome_command_ids.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/chrome_browser_main.h" |
| #include "chrome/browser/chrome_notification_types.h" |
| #include "chrome/browser/download/download_browsertest.h" |
| #include "chrome/browser/download/download_prefs.h" |
| #include "chrome/browser/extensions/api/web_navigation/web_navigation_api.h" |
| #include "chrome/browser/extensions/extension_apitest.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/renderer_context_menu/render_view_context_menu_test_util.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_navigator_params.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/render_widget_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/context_menu_params.h" |
| #include "content/public/common/resource_type.h" |
| #include "content/public/common/url_constants.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/test_utils.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/common/switches.h" |
| #include "extensions/test/result_catcher.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "net/test/embedded_test_server/http_request.h" |
| #include "net/test/embedded_test_server/http_response.h" |
| #include "third_party/blink/public/platform/web_input_event.h" |
| #include "third_party/blink/public/web/web_context_menu_data.h" |
| |
| using content::ResourceType; |
| using content::WebContents; |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // Waits for a WC to be created. Once it starts loading |delay_url| (after at |
| // least the first navigation has committed), it delays the load, executes |
| // |script| in the last committed RVH and resumes the load when a URL ending in |
| // |until_url_suffix| commits. This class expects |script| to trigger the load |
| // of an URL ending in |until_url_suffix|. |
| class DelayLoadStartAndExecuteJavascript |
| : public content::NotificationObserver, |
| public content::WebContentsObserver { |
| public: |
| DelayLoadStartAndExecuteJavascript( |
| const GURL& delay_url, |
| const std::string& script, |
| const std::string& until_url_suffix) |
| : content::WebContentsObserver(), |
| delay_url_(delay_url), |
| until_url_suffix_(until_url_suffix), |
| script_(script), |
| has_user_gesture_(false), |
| script_was_executed_(false), |
| rfh_(nullptr) { |
| registrar_.Add(this, |
| chrome::NOTIFICATION_TAB_ADDED, |
| content::NotificationService::AllSources()); |
| } |
| ~DelayLoadStartAndExecuteJavascript() override {} |
| |
| void Observe(int type, |
| const content::NotificationSource& source, |
| const content::NotificationDetails& details) override { |
| if (type != chrome::NOTIFICATION_TAB_ADDED) { |
| NOTREACHED(); |
| return; |
| } |
| content::WebContentsObserver::Observe( |
| content::Details<content::WebContents>(details).ptr()); |
| registrar_.RemoveAll(); |
| } |
| |
| void DidStartNavigation( |
| content::NavigationHandle* navigation_handle) override { |
| if (navigation_handle->GetURL() != delay_url_ || !rfh_) |
| return; |
| |
| auto throttle = |
| std::make_unique<WillStartRequestObserverThrottle>(navigation_handle); |
| throttle_ = throttle->AsWeakPtr(); |
| navigation_handle->RegisterThrottleForTesting(std::move(throttle)); |
| |
| if (has_user_gesture_) { |
| rfh_->ExecuteJavaScriptWithUserGestureForTests( |
| base::UTF8ToUTF16(script_)); |
| } else { |
| rfh_->ExecuteJavaScriptForTests(base::UTF8ToUTF16(script_)); |
| } |
| script_was_executed_ = true; |
| } |
| |
| void DidFinishNavigation( |
| content::NavigationHandle* navigation_handle) override { |
| if (!navigation_handle->HasCommitted() || navigation_handle->IsErrorPage()) |
| return; |
| |
| if (script_was_executed_ && |
| base::EndsWith(navigation_handle->GetURL().spec(), until_url_suffix_, |
| base::CompareCase::SENSITIVE)) { |
| content::WebContentsObserver::Observe(NULL); |
| if (throttle_) |
| throttle_->Unblock(); |
| } |
| |
| if (navigation_handle->IsInMainFrame()) |
| rfh_ = navigation_handle->GetRenderFrameHost(); |
| } |
| |
| void set_has_user_gesture(bool has_user_gesture) { |
| has_user_gesture_ = has_user_gesture; |
| } |
| |
| private: |
| class WillStartRequestObserverThrottle |
| : public content::NavigationThrottle, |
| public base::SupportsWeakPtr<WillStartRequestObserverThrottle> { |
| public: |
| explicit WillStartRequestObserverThrottle(content::NavigationHandle* handle) |
| : NavigationThrottle(handle) {} |
| ~WillStartRequestObserverThrottle() override {} |
| |
| const char* GetNameForLogging() override { |
| return "WillStartRequestObserverThrottle"; |
| } |
| |
| void Unblock() { |
| DCHECK(throttled_); |
| Resume(); |
| } |
| |
| private: |
| NavigationThrottle::ThrottleCheckResult WillStartRequest() override { |
| throttled_ = true; |
| return NavigationThrottle::DEFER; |
| } |
| |
| bool throttled_ = false; |
| }; |
| |
| content::NotificationRegistrar registrar_; |
| |
| base::WeakPtr<WillStartRequestObserverThrottle> throttle_; |
| |
| GURL delay_url_; |
| std::string until_url_suffix_; |
| std::string script_; |
| bool has_user_gesture_; |
| bool script_was_executed_; |
| content::RenderFrameHost* rfh_; |
| |
| DISALLOW_COPY_AND_ASSIGN(DelayLoadStartAndExecuteJavascript); |
| }; |
| |
| // Handles requests for URLs with paths of "/test*" sent to the test server, so |
| // tests request a URL that receives a non-error response. |
| std::unique_ptr<net::test_server::HttpResponse> HandleTestRequest( |
| const net::test_server::HttpRequest& request) { |
| if (!base::StartsWith(request.relative_url, "/test", |
| base::CompareCase::SENSITIVE)) { |
| return nullptr; |
| } |
| std::unique_ptr<net::test_server::BasicHttpResponse> response( |
| new net::test_server::BasicHttpResponse()); |
| response->set_content("This space intentionally left blank."); |
| return std::move(response); |
| } |
| |
| } // namespace |
| |
| class WebNavigationApiTest : public ExtensionApiTest { |
| public: |
| WebNavigationApiTest() { |
| embedded_test_server()->RegisterRequestHandler( |
| base::Bind(&HandleTestRequest)); |
| } |
| ~WebNavigationApiTest() override {} |
| |
| void SetUpInProcessBrowserTestFixture() override { |
| ExtensionApiTest::SetUpInProcessBrowserTestFixture(); |
| |
| FrameNavigationState::set_allow_extension_scheme(true); |
| |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(WebNavigationApiTest); |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, Api) { |
| ASSERT_TRUE(RunExtensionTest("webnavigation/api")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, GetFrame) { |
| ASSERT_TRUE(RunExtensionTest("webnavigation/getFrame")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, ClientRedirect) { |
| ASSERT_TRUE(RunExtensionTest("webnavigation/clientRedirect")) |
| << message_; |
| } |
| |
| // http://crbug.com/660288 |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, DISABLED_ServerRedirect) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webnavigation/serverRedirect")) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, Download) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webnavigation/download")) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, ServerRedirectSingleProcess) { |
| // TODO(lukasza): https://crbug.com/671734: Investigate why this test fails |
| // with --site-per-process. |
| if (content::AreAllSitesIsolatedForTesting()) |
| return; |
| |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // Set max renderers to 1 to force running out of processes. |
| content::RenderProcessHost::SetMaxRendererProcessCount(1); |
| |
| // Wait for the extension to set itself up and return control to us. |
| ASSERT_TRUE( |
| RunExtensionTest("webnavigation/serverRedirectSingleProcess")) |
| << message_; |
| |
| WebContents* tab = browser()->tab_strip_model()->GetActiveWebContents(); |
| content::WaitForLoadStop(tab); |
| |
| ResultCatcher catcher; |
| GURL url( |
| base::StringPrintf("http://www.a.com:%u/extensions/api_test/" |
| "webnavigation/serverRedirectSingleProcess/a.html", |
| embedded_test_server()->port())); |
| |
| ui_test_utils::NavigateToURL(browser(), url); |
| |
| url = GURL(base::StringPrintf( |
| "http://www.b.com:%u/server-redirect?http://www.b.com:%u/test", |
| embedded_test_server()->port(), embedded_test_server()->port())); |
| |
| ui_test_utils::NavigateToURL(browser(), url); |
| |
| ASSERT_TRUE(catcher.GetNextResult()) << catcher.message(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, ForwardBack) { |
| ASSERT_TRUE(RunExtensionTest("webnavigation/forwardBack")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, IFrame) { |
| ASSERT_TRUE(RunExtensionTest("webnavigation/iframe")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, SrcDoc) { |
| ASSERT_TRUE(RunExtensionTest("webnavigation/srcdoc")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, OpenTab) { |
| ASSERT_TRUE(RunExtensionTest("webnavigation/openTab")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, ReferenceFragment) { |
| ASSERT_TRUE(RunExtensionTest("webnavigation/referenceFragment")) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, SimpleLoad) { |
| ASSERT_TRUE(RunExtensionTest("webnavigation/simpleLoad")) << message_; |
| } |
| |
| // Flaky on Windows, Mac and Linux. See http://crbug.com/477480 (Windows) and |
| // https://crbug.com/746407 (Mac, Linux). |
| #if defined(OS_WIN) || defined(OS_MACOSX) || defined(OS_LINUX) |
| #define MAYBE_Failures DISABLED_Failures |
| #else |
| #define MAYBE_Failures Failures |
| #endif |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, MAYBE_Failures) { |
| ASSERT_TRUE(RunExtensionTest("webnavigation/failures")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, FilteredTest) { |
| ASSERT_TRUE(RunExtensionTest("webnavigation/filtered")) << message_; |
| } |
| |
| // Flaky on Windows. See http://crbug.com/662160. |
| #if defined(OS_WIN) |
| #define MAYBE_UserAction DISABLED_UserAction |
| #else |
| #define MAYBE_UserAction UserAction |
| #endif |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, MAYBE_UserAction) { |
| content::IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess()); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // Wait for the extension to set itself up and return control to us. |
| ASSERT_TRUE(RunExtensionTest("webnavigation/userAction")) << message_; |
| |
| WebContents* tab = browser()->tab_strip_model()->GetActiveWebContents(); |
| content::WaitForLoadStop(tab); |
| |
| ResultCatcher catcher; |
| |
| ExtensionService* service = extensions::ExtensionSystem::Get( |
| browser()->profile())->extension_service(); |
| const extensions::Extension* extension = |
| service->GetExtensionById(last_loaded_extension_id(), false); |
| GURL url = extension->GetResourceURL( |
| "a.html?" + base::IntToString(embedded_test_server()->port())); |
| |
| ui_test_utils::NavigateToURL(browser(), url); |
| |
| // This corresponds to "Open link in new tab". |
| content::ContextMenuParams params; |
| params.is_editable = false; |
| params.media_type = blink::WebContextMenuData::kMediaTypeNone; |
| params.page_url = url; |
| params.link_url = extension->GetResourceURL("b.html"); |
| |
| // Get the child frame, which will be the one associated with the context |
| // menu. |
| std::vector<content::RenderFrameHost*> frames = tab->GetAllFrames(); |
| EXPECT_EQ(2UL, frames.size()); |
| EXPECT_TRUE(frames[1]->GetParent()); |
| |
| TestRenderViewContextMenu menu(frames[1], params); |
| menu.Init(); |
| menu.ExecuteCommand(IDC_CONTENT_CONTEXT_OPENLINKNEWTAB, 0); |
| |
| ASSERT_TRUE(catcher.GetNextResult()) << catcher.message(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, RequestOpenTab) { |
| |
| // Wait for the extension to set itself up and return control to us. |
| ASSERT_TRUE(RunExtensionTest("webnavigation/requestOpenTab")) |
| << message_; |
| |
| WebContents* tab = browser()->tab_strip_model()->GetActiveWebContents(); |
| content::WaitForLoadStop(tab); |
| |
| ResultCatcher catcher; |
| |
| ExtensionService* service = extensions::ExtensionSystem::Get( |
| browser()->profile())->extension_service(); |
| const extensions::Extension* extension = |
| service->GetExtensionById(last_loaded_extension_id(), false); |
| GURL url = extension->GetResourceURL("a.html"); |
| |
| ui_test_utils::NavigateToURL(browser(), url); |
| |
| // There's a link on a.html. Middle-click on it to open it in a new tab. |
| blink::WebMouseEvent mouse_event( |
| blink::WebInputEvent::kMouseDown, blink::WebInputEvent::kNoModifiers, |
| blink::WebInputEvent::GetStaticTimeStampForTests()); |
| mouse_event.button = blink::WebMouseEvent::Button::kMiddle; |
| mouse_event.SetPositionInWidget(7, 7); |
| mouse_event.click_count = 1; |
| tab->GetRenderViewHost()->GetWidget()->ForwardMouseEvent(mouse_event); |
| mouse_event.SetType(blink::WebInputEvent::kMouseUp); |
| tab->GetRenderViewHost()->GetWidget()->ForwardMouseEvent(mouse_event); |
| |
| ASSERT_TRUE(catcher.GetNextResult()) << catcher.message(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, TargetBlank) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // Wait for the extension to set itself up and return control to us. |
| ASSERT_TRUE(RunExtensionTest("webnavigation/targetBlank")) << message_; |
| |
| WebContents* tab = browser()->tab_strip_model()->GetActiveWebContents(); |
| content::WaitForLoadStop(tab); |
| |
| ResultCatcher catcher; |
| |
| GURL url = embedded_test_server()->GetURL( |
| "/extensions/api_test/webnavigation/targetBlank/a.html"); |
| |
| NavigateParams params(browser(), url, ui::PAGE_TRANSITION_LINK); |
| ui_test_utils::NavigateToURL(¶ms); |
| |
| // There's a link with target=_blank on a.html. Click on it to open it in a |
| // new tab. |
| blink::WebMouseEvent mouse_event( |
| blink::WebInputEvent::kMouseDown, blink::WebInputEvent::kNoModifiers, |
| blink::WebInputEvent::GetStaticTimeStampForTests()); |
| mouse_event.button = blink::WebMouseEvent::Button::kLeft; |
| mouse_event.SetPositionInWidget(7, 7); |
| mouse_event.click_count = 1; |
| tab->GetRenderViewHost()->GetWidget()->ForwardMouseEvent(mouse_event); |
| mouse_event.SetType(blink::WebInputEvent::kMouseUp); |
| tab->GetRenderViewHost()->GetWidget()->ForwardMouseEvent(mouse_event); |
| |
| ASSERT_TRUE(catcher.GetNextResult()) << catcher.message(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, TargetBlankIncognito) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // Wait for the extension to set itself up and return control to us. |
| ASSERT_TRUE(RunExtensionTestIncognito("webnavigation/targetBlank")) |
| << message_; |
| |
| ResultCatcher catcher; |
| |
| GURL url = embedded_test_server()->GetURL( |
| "/extensions/api_test/webnavigation/targetBlank/a.html"); |
| |
| Browser* otr_browser = OpenURLOffTheRecord(browser()->profile(), url); |
| WebContents* tab = otr_browser->tab_strip_model()->GetActiveWebContents(); |
| |
| // There's a link with target=_blank on a.html. Click on it to open it in a |
| // new tab. |
| blink::WebMouseEvent mouse_event( |
| blink::WebInputEvent::kMouseDown, blink::WebInputEvent::kNoModifiers, |
| blink::WebInputEvent::GetStaticTimeStampForTests()); |
| mouse_event.button = blink::WebMouseEvent::Button::kLeft; |
| mouse_event.SetPositionInWidget(7, 7); |
| mouse_event.click_count = 1; |
| tab->GetRenderViewHost()->GetWidget()->ForwardMouseEvent(mouse_event); |
| mouse_event.SetType(blink::WebInputEvent::kMouseUp); |
| tab->GetRenderViewHost()->GetWidget()->ForwardMouseEvent(mouse_event); |
| |
| ASSERT_TRUE(catcher.GetNextResult()) << catcher.message(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, History) { |
| ASSERT_TRUE(RunExtensionTest("webnavigation/history")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, CrossProcess) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| LoadExtension(test_data_dir_.AppendASCII("webnavigation").AppendASCII("app")); |
| |
| // See crossProcess/d.html. |
| DelayLoadStartAndExecuteJavascript call_script( |
| embedded_test_server()->GetURL("/test1"), |
| "navigate2()", |
| "empty.html"); |
| |
| DelayLoadStartAndExecuteJavascript call_script_user_gesture( |
| embedded_test_server()->GetURL("/test2"), |
| "navigate2()", |
| "empty.html"); |
| call_script_user_gesture.set_has_user_gesture(true); |
| |
| ASSERT_TRUE(RunExtensionTest("webnavigation/crossProcess")) << message_; |
| } |
| |
| // crbug.com/708139. |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, DISABLED_CrossProcessFragment) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // See crossProcessFragment/f.html. |
| DelayLoadStartAndExecuteJavascript call_script3( |
| embedded_test_server()->GetURL("/test3"), |
| "updateFragment()", |
| base::StringPrintf("f.html?%u#foo", embedded_test_server()->port())); |
| |
| // See crossProcessFragment/g.html. |
| DelayLoadStartAndExecuteJavascript call_script4( |
| embedded_test_server()->GetURL("/test4"), |
| "updateFragment()", |
| base::StringPrintf("g.html?%u#foo", embedded_test_server()->port())); |
| |
| ASSERT_TRUE(RunExtensionTest("webnavigation/crossProcessFragment")) |
| << message_; |
| } |
| |
| // crbug.com/708139. |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, DISABLED_CrossProcessHistory) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // See crossProcessHistory/e.html. |
| DelayLoadStartAndExecuteJavascript call_script2( |
| embedded_test_server()->GetURL("/test2"), |
| "updateHistory()", |
| "empty.html"); |
| |
| // See crossProcessHistory/h.html. |
| DelayLoadStartAndExecuteJavascript call_script5( |
| embedded_test_server()->GetURL("/test5"), |
| "updateHistory()", |
| "empty.html"); |
| |
| // See crossProcessHistory/i.html. |
| DelayLoadStartAndExecuteJavascript call_script6( |
| embedded_test_server()->GetURL("/test6"), |
| "updateHistory()", |
| "empty.html"); |
| |
| ASSERT_TRUE(RunExtensionTest("webnavigation/crossProcessHistory")) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, CrossProcessIframe) { |
| content::IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess()); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("webnavigation/crossProcessIframe")) << message_; |
| } |
| |
| // TODO(jam): http://crbug.com/350550 |
| #if !(defined(OS_CHROMEOS) && defined(ADDRESS_SANITIZER)) |
| IN_PROC_BROWSER_TEST_F(WebNavigationApiTest, Crash) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // Wait for the extension to set itself up and return control to us. |
| ASSERT_TRUE(RunExtensionTest("webnavigation/crash")) << message_; |
| |
| WebContents* tab = browser()->tab_strip_model()->GetActiveWebContents(); |
| content::WaitForLoadStop(tab); |
| |
| ResultCatcher catcher; |
| |
| GURL url(base::StringPrintf( |
| "http://www.a.com:%u/" |
| "extensions/api_test/webnavigation/crash/a.html", |
| embedded_test_server()->port())); |
| ui_test_utils::NavigateToURL(browser(), url); |
| |
| ui_test_utils::NavigateToURL(browser(), GURL(content::kChromeUICrashURL)); |
| |
| url = GURL(base::StringPrintf( |
| "http://www.a.com:%u/" |
| "extensions/api_test/webnavigation/crash/b.html", |
| embedded_test_server()->port())); |
| ui_test_utils::NavigateToURL(browser(), url); |
| |
| ASSERT_TRUE(catcher.GetNextResult()) << catcher.message(); |
| } |
| |
| #endif |
| |
| } // namespace extensions |