|  | // Copyright 2014 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/extensions/api/chrome_extensions_api_client.h" | 
|  |  | 
|  | #include <memory> | 
|  | #include <utility> | 
|  |  | 
|  | #include "base/check.h" | 
|  | #include "base/files/file_path.h" | 
|  | #include "base/functional/bind.h" | 
|  | #include "base/memory/raw_ptr.h" | 
|  | #include "base/memory/scoped_refptr.h" | 
|  | #include "base/strings/string_util.h" | 
|  | #include "base/task/single_thread_task_runner.h" | 
|  | #include "build/build_config.h" | 
|  | #include "build/chromeos_buildflags.h" | 
|  | #include "chrome/browser/extensions/api/automation_internal/chrome_automation_internal_api_delegate.h" | 
|  | #include "chrome/browser/extensions/api/declarative_content/chrome_content_rules_registry.h" | 
|  | #include "chrome/browser/extensions/api/declarative_content/default_content_predicate_evaluators.h" | 
|  | #include "chrome/browser/extensions/api/management/chrome_management_api_delegate.h" | 
|  | #include "chrome/browser/extensions/api/messaging/chrome_messaging_delegate.h" | 
|  | #include "chrome/browser/extensions/api/messaging/chrome_native_message_port_dispatcher.h" | 
|  | #include "chrome/browser/extensions/api/metrics_private/chrome_metrics_private_delegate.h" | 
|  | #include "chrome/browser/extensions/api/storage/managed_value_store_cache.h" | 
|  | #include "chrome/browser/extensions/api/storage/sync_value_store_cache.h" | 
|  | #include "chrome/browser/extensions/extension_action_dispatcher.h" | 
|  | #include "chrome/browser/extensions/extension_action_runner.h" | 
|  | #include "chrome/browser/extensions/extension_tab_util.h" | 
|  | #include "chrome/browser/favicon/favicon_utils.h" | 
|  | #include "chrome/browser/ui/webui/devtools/devtools_ui.h" | 
|  | #include "chrome/common/buildflags.h" | 
|  | #include "chrome/common/url_constants.h" | 
|  | #include "chrome/common/webui_url_constants.h" | 
|  | #include "components/signin/core/browser/signin_header_helper.h" | 
|  | #include "components/value_store/value_store_factory.h" | 
|  | #include "content/public/browser/browser_context.h" | 
|  | #include "content/public/browser/browser_task_traits.h" | 
|  | #include "content/public/browser/browser_thread.h" | 
|  | #include "content/public/browser/render_frame_host.h" | 
|  | #include "extensions/browser/api/messaging/messaging_delegate.h" | 
|  | #include "extensions/browser/api/messaging/native_message_host.h" | 
|  | #include "extensions/browser/api/messaging/native_message_port.h" | 
|  | #include "extensions/browser/api/virtual_keyboard_private/virtual_keyboard_delegate.h" | 
|  | #include "extensions/browser/api/web_request/web_request_info.h" | 
|  | #include "extensions/browser/extension_action.h" | 
|  | #include "extensions/browser/extension_action_manager.h" | 
|  | #include "extensions/browser/extension_registry.h" | 
|  | #include "extensions/browser/supervised_user_extensions_delegate.h" | 
|  | #include "google_apis/gaia/gaia_urls.h" | 
|  | #include "pdf/buildflags.h" | 
|  | #include "printing/buildflags/buildflags.h" | 
|  | #include "services/network/public/mojom/fetch_api.mojom-shared.h" | 
|  | #include "ui/base/page_transition_types.h" | 
|  | #include "ui/base/window_open_disposition.h" | 
|  | #include "url/gurl.h" | 
|  |  | 
|  | #if BUILDFLAG(ENABLE_GUEST_VIEW) | 
|  | #include "chrome/browser/guest_view/app_view/chrome_app_view_guest_delegate.h" | 
|  | #include "chrome/browser/guest_view/chrome_guest_view_manager_delegate.h" | 
|  | #include "chrome/browser/guest_view/extension_options/chrome_extension_options_guest_delegate.h" | 
|  | #include "chrome/browser/guest_view/mime_handler_view/chrome_mime_handler_view_guest_delegate.h" | 
|  | #include "chrome/browser/guest_view/web_view/chrome_web_view_guest_delegate.h" | 
|  | #include "chrome/browser/guest_view/web_view/chrome_web_view_permission_helper_delegate.h" | 
|  | #include "extensions/browser/guest_view/web_view/web_view_guest.h" | 
|  | #include "extensions/browser/guest_view/web_view/web_view_permission_helper.h" | 
|  | #endif | 
|  |  | 
|  | #if !BUILDFLAG(IS_ANDROID) | 
|  | #include "chrome/browser/extensions/api/feedback_private/chrome_feedback_private_delegate.h" | 
|  | #include "chrome/browser/extensions/api/file_system/chrome_file_system_delegate.h" | 
|  | #include "chrome/browser/search/instant_service.h" | 
|  | #include "chrome/browser/search/instant_service_factory.h" | 
|  | #endif | 
|  |  | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | #include "chrome/browser/extensions/api/file_handlers/non_native_file_system_delegate_chromeos.h" | 
|  | #include "chrome/browser/extensions/api/file_system/chrome_file_system_delegate_ash.h" | 
|  | #include "chrome/browser/extensions/api/file_system/consent_provider_impl.h" | 
|  | #include "chrome/browser/extensions/api/media_perception_private/media_perception_api_delegate_chromeos.h" | 
|  | #include "chrome/browser/extensions/api/virtual_keyboard_private/chrome_virtual_keyboard_delegate.h" | 
|  | #include "chrome/browser/extensions/clipboard_extension_helper_chromeos.h" | 
|  | #include "chromeos/ash/components/settings/cros_settings.h" | 
|  | #endif | 
|  |  | 
|  | #if BUILDFLAG(ENABLE_PRINTING) | 
|  | #include "chrome/browser/printing/printing_init.h" | 
|  | #endif | 
|  |  | 
|  | namespace extensions { | 
|  |  | 
|  | ChromeExtensionsAPIClient::ChromeExtensionsAPIClient() = default; | 
|  |  | 
|  | ChromeExtensionsAPIClient::~ChromeExtensionsAPIClient() = default; | 
|  |  | 
|  | void ChromeExtensionsAPIClient::AddAdditionalValueStoreCaches( | 
|  | content::BrowserContext* context, | 
|  | const scoped_refptr<value_store::ValueStoreFactory>& factory, | 
|  | SettingsChangedCallback observer, | 
|  | std::map<settings_namespace::Namespace, | 
|  | raw_ptr<ValueStoreCache, CtnExperimental>>* caches) { | 
|  | // Add support for chrome.storage.sync. | 
|  | (*caches)[settings_namespace::SYNC] = | 
|  | new SyncValueStoreCache(factory, observer, context->GetPath()); | 
|  |  | 
|  | // Add support for chrome.storage.managed. | 
|  | (*caches)[settings_namespace::MANAGED] = new ManagedValueStoreCache( | 
|  | *Profile::FromBrowserContext(context), factory, observer); | 
|  | } | 
|  |  | 
|  | void ChromeExtensionsAPIClient::AttachWebContentsHelpers( | 
|  | content::WebContents* web_contents) const { | 
|  | favicon::CreateContentFaviconDriverForWebContents(web_contents); | 
|  | #if BUILDFLAG(ENABLE_PRINTING) | 
|  | printing::InitializePrintingForWebContents(web_contents); | 
|  | #endif | 
|  | } | 
|  |  | 
|  | bool ChromeExtensionsAPIClient::ShouldHideResponseHeader( | 
|  | const GURL& url, | 
|  | const std::string& header_name) const { | 
|  | // Gaia may send a OAUth2 authorization code in the Dice response header, | 
|  | // which could allow an extension to generate a refresh token for the account. | 
|  | return url.host_piece() == GaiaUrls::GetInstance()->gaia_url().host_piece() && | 
|  | base::CompareCaseInsensitiveASCII(header_name, | 
|  | signin::kDiceResponseHeader) == 0; | 
|  | } | 
|  |  | 
|  | bool ChromeExtensionsAPIClient::ShouldHideBrowserNetworkRequest( | 
|  | content::BrowserContext* context, | 
|  | const WebRequestInfo& request) const { | 
|  | DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | 
|  |  | 
|  | // Note: browser initiated non-navigation requests are hidden from extensions. | 
|  | // But we do still need to protect some sensitive sub-frame navigation | 
|  | // requests. | 
|  | // Exclude main frame navigation requests. | 
|  | bool is_browser_request = | 
|  | request.render_process_id == -1 && | 
|  | request.web_request_type != WebRequestResourceType::MAIN_FRAME; | 
|  |  | 
|  | // Hide requests made by the Devtools frontend. | 
|  | bool is_sensitive_request = | 
|  | is_browser_request && DevToolsUI::IsFrontendResourceURL(request.url); | 
|  |  | 
|  | // Hide requests made by the browser on behalf of the NTP. | 
|  | is_sensitive_request |= | 
|  | is_browser_request && | 
|  | request.initiator == | 
|  | url::Origin::Create(GURL(chrome::kChromeUINewTabURL)); | 
|  |  | 
|  | // Hide requests made by the browser on behalf of the 1P WebUI NTP. | 
|  | is_sensitive_request |= | 
|  | is_browser_request && | 
|  | request.initiator == | 
|  | url::Origin::Create(GURL(chrome::kChromeUINewTabPageURL)); | 
|  |  | 
|  | // Android does not support instant. | 
|  | #if !BUILDFLAG(IS_ANDROID) | 
|  | // Hide requests made by the NTP Instant renderer. | 
|  | auto* instant_service = | 
|  | context | 
|  | ? InstantServiceFactory::GetForProfile(static_cast<Profile*>(context)) | 
|  | : nullptr; | 
|  | if (instant_service) { | 
|  | is_sensitive_request |= | 
|  | instant_service->IsInstantProcess(request.render_process_id); | 
|  | } | 
|  | #endif  // !BUILDFLAG(IS_ANDROID) | 
|  |  | 
|  | return is_sensitive_request; | 
|  | } | 
|  |  | 
|  | void ChromeExtensionsAPIClient::NotifyWebRequestWithheld( | 
|  | int render_process_id, | 
|  | int render_frame_id, | 
|  | const ExtensionId& extension_id) { | 
|  | DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | 
|  |  | 
|  | // Track down the ExtensionActionRunner and the extension. Since this is | 
|  | // asynchronous, we could hit a null anywhere along the path. | 
|  | content::RenderFrameHost* render_frame_host = | 
|  | content::RenderFrameHost::FromID(render_process_id, render_frame_id); | 
|  | if (!render_frame_host) { | 
|  | return; | 
|  | } | 
|  | // We don't count subframes and prerendering blocked actions as yet, since | 
|  | // there's no way to surface this to the user. Ignore these (which is also | 
|  | // what we do for content scripts). | 
|  | if (!render_frame_host->IsInPrimaryMainFrame()) { | 
|  | return; | 
|  | } | 
|  | content::WebContents* web_contents = | 
|  | content::WebContents::FromRenderFrameHost(render_frame_host); | 
|  | if (!web_contents) { | 
|  | return; | 
|  | } | 
|  | extensions::ExtensionActionRunner* runner = | 
|  | extensions::ExtensionActionRunner::GetForWebContents(web_contents); | 
|  | if (!runner) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | const extensions::Extension* extension = | 
|  | extensions::ExtensionRegistry::Get(web_contents->GetBrowserContext()) | 
|  | ->enabled_extensions() | 
|  | .GetByID(extension_id); | 
|  | if (!extension) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // If the extension doesn't request access to the tab, return. The user | 
|  | // invoking the extension on a site grants access to the tab's origin if | 
|  | // and only if the extension requested it; without requesting the tab, | 
|  | // clicking on the extension won't grant access to the resource. | 
|  | // https://crbug.com/891586. | 
|  | // TODO(crbug.com/40076508): We can remove this if extensions require host | 
|  | // permissions to the initiator, since then we'll never get into this type | 
|  | // of circumstance (the request would be blocked, rather than withheld). | 
|  | if (!extension->permissions_data() | 
|  | ->withheld_permissions() | 
|  | .explicit_hosts() | 
|  | .MatchesURL(render_frame_host->GetLastCommittedURL())) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | runner->OnWebRequestBlocked(extension); | 
|  | } | 
|  |  | 
|  | void ChromeExtensionsAPIClient::UpdateActionCount( | 
|  | content::BrowserContext* context, | 
|  | const ExtensionId& extension_id, | 
|  | int tab_id, | 
|  | int action_count, | 
|  | bool clear_badge_text) { | 
|  | const Extension* extension = | 
|  | ExtensionRegistry::Get(context)->enabled_extensions().GetByID( | 
|  | extension_id); | 
|  | DCHECK(extension); | 
|  |  | 
|  | ExtensionAction* action = | 
|  | ExtensionActionManager::Get(context)->GetExtensionAction(*extension); | 
|  | DCHECK(action); | 
|  |  | 
|  | action->SetDNRActionCount(tab_id, action_count); | 
|  |  | 
|  | // The badge text should be cleared if |action| contains explicitly set badge | 
|  | // text for the |tab_id| when the preference is then toggled on. In this case, | 
|  | // the matched action count should take precedence over the badge text. | 
|  | if (clear_badge_text) { | 
|  | action->ClearBadgeText(tab_id); | 
|  | } | 
|  |  | 
|  | content::WebContents* tab_contents = nullptr; | 
|  | if (ExtensionTabUtil::GetTabById(tab_id, context, /*include_incognito=*/true, | 
|  | &tab_contents) && | 
|  | tab_contents) { | 
|  | ExtensionActionDispatcher::Get(context)->NotifyChange(action, tab_contents, | 
|  | context); | 
|  | } | 
|  | } | 
|  |  | 
|  | void ChromeExtensionsAPIClient::ClearActionCount( | 
|  | content::BrowserContext* context, | 
|  | const Extension& extension) { | 
|  | ExtensionAction* action = | 
|  | ExtensionActionManager::Get(context)->GetExtensionAction(extension); | 
|  | DCHECK(action); | 
|  |  | 
|  | action->ClearDNRActionCountForAllTabs(); | 
|  |  | 
|  | std::vector<content::WebContents*> contents_to_notify = | 
|  | ExtensionTabUtil::GetAllActiveWebContentsForContext( | 
|  | context, /*include_incognito=*/true); | 
|  |  | 
|  | for (auto* active_contents : contents_to_notify) { | 
|  | ExtensionActionDispatcher::Get(context)->NotifyChange( | 
|  | action, active_contents, context); | 
|  | } | 
|  | } | 
|  |  | 
|  | #if BUILDFLAG(ENABLE_GUEST_VIEW) | 
|  | std::unique_ptr<AppViewGuestDelegate> | 
|  | ChromeExtensionsAPIClient::CreateAppViewGuestDelegate() const { | 
|  | return std::make_unique<ChromeAppViewGuestDelegate>(); | 
|  | } | 
|  |  | 
|  | std::unique_ptr<ExtensionOptionsGuestDelegate> | 
|  | ChromeExtensionsAPIClient::CreateExtensionOptionsGuestDelegate( | 
|  | ExtensionOptionsGuest* guest) const { | 
|  | return std::make_unique<ChromeExtensionOptionsGuestDelegate>(guest); | 
|  | } | 
|  |  | 
|  | std::unique_ptr<guest_view::GuestViewManagerDelegate> | 
|  | ChromeExtensionsAPIClient::CreateGuestViewManagerDelegate() const { | 
|  | return std::make_unique<ChromeGuestViewManagerDelegate>(); | 
|  | } | 
|  |  | 
|  | std::unique_ptr<MimeHandlerViewGuestDelegate> | 
|  | ChromeExtensionsAPIClient::CreateMimeHandlerViewGuestDelegate( | 
|  | MimeHandlerViewGuest* guest) const { | 
|  | return std::make_unique<ChromeMimeHandlerViewGuestDelegate>(); | 
|  | } | 
|  |  | 
|  | std::unique_ptr<WebViewGuestDelegate> | 
|  | ChromeExtensionsAPIClient::CreateWebViewGuestDelegate( | 
|  | WebViewGuest* web_view_guest) const { | 
|  | return std::make_unique<ChromeWebViewGuestDelegate>(web_view_guest); | 
|  | } | 
|  |  | 
|  | std::unique_ptr<WebViewPermissionHelperDelegate> | 
|  | ChromeExtensionsAPIClient::CreateWebViewPermissionHelperDelegate( | 
|  | WebViewPermissionHelper* web_view_permission_helper) const { | 
|  | return std::make_unique<ChromeWebViewPermissionHelperDelegate>( | 
|  | web_view_permission_helper); | 
|  | } | 
|  | #endif  // BUILDFLAG(ENABLE_GUEST_VIEW) | 
|  |  | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | std::unique_ptr<ConsentProvider> | 
|  | ChromeExtensionsAPIClient::CreateConsentProvider( | 
|  | content::BrowserContext* browser_context) const { | 
|  | auto consent_provider_delegate = | 
|  | std::make_unique<file_system_api::ConsentProviderDelegate>( | 
|  | Profile::FromBrowserContext(browser_context)); | 
|  | return std::make_unique<file_system_api::ConsentProviderImpl>( | 
|  | std::move(consent_provider_delegate)); | 
|  | } | 
|  | #endif  // BUILDFLAG(IS_CHROMEOS) | 
|  |  | 
|  | scoped_refptr<ContentRulesRegistry> | 
|  | ChromeExtensionsAPIClient::CreateContentRulesRegistry( | 
|  | content::BrowserContext* browser_context, | 
|  | RulesCacheDelegate* cache_delegate) const { | 
|  | return base::MakeRefCounted<ChromeContentRulesRegistry>( | 
|  | browser_context, cache_delegate, | 
|  | base::BindOnce(&CreateDefaultContentPredicateEvaluators, | 
|  | base::Unretained(browser_context))); | 
|  | } | 
|  |  | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | bool ChromeExtensionsAPIClient::ShouldAllowDetachingUsb(int vid, | 
|  | int pid) const { | 
|  | const base::Value::List* policy_list; | 
|  | if (ash::CrosSettings::Get()->GetList(ash::kUsbDetachableAllowlist, | 
|  | &policy_list)) { | 
|  | for (const auto& entry : *policy_list) { | 
|  | const base::Value::Dict* entry_dict = entry.GetIfDict(); | 
|  | if (entry_dict && | 
|  | entry_dict->FindInt(ash::kUsbDetachableAllowlistKeyVid) == vid && | 
|  | entry_dict->FindInt(ash::kUsbDetachableAllowlistKeyPid) == pid) { | 
|  | return true; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | return false; | 
|  | } | 
|  | #endif  // BUILDFLAG(IS_CHROMEOS) | 
|  |  | 
|  | std::unique_ptr<VirtualKeyboardDelegate> | 
|  | ChromeExtensionsAPIClient::CreateVirtualKeyboardDelegate( | 
|  | content::BrowserContext* browser_context) const { | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | return std::make_unique<ChromeVirtualKeyboardDelegate>(browser_context); | 
|  | #else | 
|  | return nullptr; | 
|  | #endif | 
|  | } | 
|  |  | 
|  | ManagementAPIDelegate* ChromeExtensionsAPIClient::CreateManagementAPIDelegate() | 
|  | const { | 
|  | return new ChromeManagementAPIDelegate; | 
|  | } | 
|  |  | 
|  | MetricsPrivateDelegate* ChromeExtensionsAPIClient::GetMetricsPrivateDelegate() { | 
|  | if (!metrics_private_delegate_) { | 
|  | metrics_private_delegate_ = | 
|  | std::make_unique<ChromeMetricsPrivateDelegate>(); | 
|  | } | 
|  | return metrics_private_delegate_.get(); | 
|  | } | 
|  |  | 
|  | MessagingDelegate* ChromeExtensionsAPIClient::GetMessagingDelegate() { | 
|  | if (!messaging_delegate_) { | 
|  | messaging_delegate_ = std::make_unique<ChromeMessagingDelegate>(); | 
|  | } | 
|  | return messaging_delegate_.get(); | 
|  | } | 
|  |  | 
|  | // The APIs that require these methods are not supported on Android. | 
|  | #if !BUILDFLAG(IS_ANDROID) | 
|  | FileSystemDelegate* ChromeExtensionsAPIClient::GetFileSystemDelegate() { | 
|  | if (!file_system_delegate_) { | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | file_system_delegate_ = std::make_unique<ChromeFileSystemDelegateAsh>(); | 
|  | #else | 
|  | file_system_delegate_ = std::make_unique<ChromeFileSystemDelegate>(); | 
|  | #endif | 
|  | } | 
|  | return file_system_delegate_.get(); | 
|  | } | 
|  |  | 
|  | FeedbackPrivateDelegate* | 
|  | ChromeExtensionsAPIClient::GetFeedbackPrivateDelegate() { | 
|  | if (!feedback_private_delegate_) { | 
|  | feedback_private_delegate_ = | 
|  | std::make_unique<ChromeFeedbackPrivateDelegate>(); | 
|  | } | 
|  | return feedback_private_delegate_.get(); | 
|  | } | 
|  |  | 
|  | AutomationInternalApiDelegate* | 
|  | ChromeExtensionsAPIClient::GetAutomationInternalApiDelegate() { | 
|  | if (!extensions_automation_api_delegate_) { | 
|  | extensions_automation_api_delegate_ = | 
|  | std::make_unique<ChromeAutomationInternalApiDelegate>(); | 
|  | } | 
|  | return extensions_automation_api_delegate_.get(); | 
|  | } | 
|  | #endif  // !BUILDFLAG(IS_ANDROID) | 
|  |  | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | MediaPerceptionAPIDelegate* | 
|  | ChromeExtensionsAPIClient::GetMediaPerceptionAPIDelegate() { | 
|  | if (!media_perception_api_delegate_) { | 
|  | media_perception_api_delegate_ = | 
|  | std::make_unique<MediaPerceptionAPIDelegateChromeOS>(); | 
|  | } | 
|  | return media_perception_api_delegate_.get(); | 
|  | } | 
|  |  | 
|  | NonNativeFileSystemDelegate* | 
|  | ChromeExtensionsAPIClient::GetNonNativeFileSystemDelegate() { | 
|  | if (!non_native_file_system_delegate_) { | 
|  | non_native_file_system_delegate_ = | 
|  | std::make_unique<NonNativeFileSystemDelegateChromeOS>(); | 
|  | } | 
|  | return non_native_file_system_delegate_.get(); | 
|  | } | 
|  |  | 
|  | void ChromeExtensionsAPIClient::SaveImageDataToClipboard( | 
|  | std::vector<uint8_t> image_data, | 
|  | api::clipboard::ImageType type, | 
|  | AdditionalDataItemList additional_items, | 
|  | base::OnceClosure success_callback, | 
|  | base::OnceCallback<void(const std::string&)> error_callback) { | 
|  | if (!clipboard_extension_helper_) { | 
|  | clipboard_extension_helper_ = std::make_unique<ClipboardExtensionHelper>(); | 
|  | } | 
|  | clipboard_extension_helper_->DecodeAndSaveImageData( | 
|  | std::move(image_data), type, std::move(additional_items), | 
|  | std::move(success_callback), std::move(error_callback)); | 
|  | } | 
|  | #endif  // BUILDFLAG(IS_CHROMEOS) | 
|  |  | 
|  | std::unique_ptr<NativeMessagePortDispatcher> | 
|  | ChromeExtensionsAPIClient::CreateNativeMessagePortDispatcher( | 
|  | std::unique_ptr<NativeMessageHost> host, | 
|  | base::WeakPtr<NativeMessagePort> port, | 
|  | scoped_refptr<base::SingleThreadTaskRunner> message_service_task_runner) { | 
|  | return std::make_unique<ChromeNativeMessagePortDispatcher>( | 
|  | std::move(host), std::move(port), std::move(message_service_task_runner)); | 
|  | } | 
|  |  | 
|  | }  // namespace extensions |