1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/extensions/api/extension_action/extension_action_api.h"
7 #include "base/lazy_instance.h"
8 #include "base/location.h"
9 #include "base/single_thread_task_runner.h"
10 #include "base/strings/string_number_conversions.h"
11 #include "base/thread_task_runner_handle.h"
12 #include "base/values.h"
13 #include "chrome/browser/extensions/active_script_controller.h"
14 #include "chrome/browser/extensions/extension_action_manager.h"
15 #include "chrome/browser/extensions/extension_tab_util.h"
16 #include "chrome/browser/extensions/extension_util.h"
17 #include "chrome/browser/extensions/tab_helper.h"
18 #include "chrome/browser/profiles/profile.h"
19 #include "chrome/browser/sessions/session_tab_helper.h"
20 #include "chrome/browser/ui/browser.h"
21 #include "chrome/browser/ui/browser_finder.h"
22 #include "chrome/browser/ui/browser_window.h"
23 #include "chrome/browser/ui/location_bar/location_bar.h"
24 #include "chrome/browser/ui/tabs/tab_strip_model.h"
25 #include "chrome/browser/ui/toolbar/toolbar_actions_bar.h"
26 #include "chrome/common/extensions/api/extension_action/action_info.h"
27 #include "content/public/browser/notification_service.h"
28 #include "extensions/browser/event_router.h"
29 #include "extensions/browser/extension_function_registry.h"
30 #include "extensions/browser/extension_host.h"
31 #include "extensions/browser/extension_registry.h"
32 #include "extensions/browser/notification_types.h"
33 #include "extensions/common/error_utils.h"
34 #include "extensions/common/feature_switch.h"
35 #include "extensions/common/image_util.h"
36 #include "ui/gfx/image/image.h"
37 #include "ui/gfx/image/image_skia.h"
39 using content::WebContents
;
41 namespace extensions
{
45 // Whether the browser action is visible in the toolbar.
46 const char kBrowserActionVisible
[] = "browser_action_visible";
49 const char kNoExtensionActionError
[] =
50 "This extension has no action specified.";
51 const char kNoTabError
[] = "No tab with id: *.";
52 const char kOpenPopupError
[] =
53 "Failed to show popup either because there is an existing popup or another "
59 // ExtensionActionAPI::Observer
62 void ExtensionActionAPI::Observer::OnExtensionActionUpdated(
63 ExtensionAction
* extension_action
,
64 content::WebContents
* web_contents
,
65 content::BrowserContext
* browser_context
) {
68 void ExtensionActionAPI::Observer::OnExtensionActionVisibilityChanged(
69 const std::string
& extension_id
,
70 bool is_now_visible
) {
73 void ExtensionActionAPI::Observer::OnPageActionsUpdated(
74 content::WebContents
* web_contents
) {
77 void ExtensionActionAPI::Observer::OnExtensionActionAPIShuttingDown() {
80 ExtensionActionAPI::Observer::~Observer() {
87 static base::LazyInstance
<BrowserContextKeyedAPIFactory
<ExtensionActionAPI
> >
88 g_factory
= LAZY_INSTANCE_INITIALIZER
;
90 ExtensionActionAPI::ExtensionActionAPI(content::BrowserContext
* context
)
91 : browser_context_(context
),
92 extension_prefs_(nullptr) {
93 ExtensionFunctionRegistry
* registry
=
94 ExtensionFunctionRegistry::GetInstance();
97 registry
->RegisterFunction
<BrowserActionSetIconFunction
>();
98 registry
->RegisterFunction
<BrowserActionSetTitleFunction
>();
99 registry
->RegisterFunction
<BrowserActionSetBadgeTextFunction
>();
100 registry
->RegisterFunction
<BrowserActionSetBadgeBackgroundColorFunction
>();
101 registry
->RegisterFunction
<BrowserActionSetPopupFunction
>();
102 registry
->RegisterFunction
<BrowserActionGetTitleFunction
>();
103 registry
->RegisterFunction
<BrowserActionGetBadgeTextFunction
>();
104 registry
->RegisterFunction
<BrowserActionGetBadgeBackgroundColorFunction
>();
105 registry
->RegisterFunction
<BrowserActionGetPopupFunction
>();
106 registry
->RegisterFunction
<BrowserActionEnableFunction
>();
107 registry
->RegisterFunction
<BrowserActionDisableFunction
>();
108 registry
->RegisterFunction
<BrowserActionOpenPopupFunction
>();
111 registry
->RegisterFunction
<PageActionShowFunction
>();
112 registry
->RegisterFunction
<PageActionHideFunction
>();
113 registry
->RegisterFunction
<PageActionSetIconFunction
>();
114 registry
->RegisterFunction
<PageActionSetTitleFunction
>();
115 registry
->RegisterFunction
<PageActionSetPopupFunction
>();
116 registry
->RegisterFunction
<PageActionGetTitleFunction
>();
117 registry
->RegisterFunction
<PageActionGetPopupFunction
>();
120 ExtensionActionAPI::~ExtensionActionAPI() {
124 BrowserContextKeyedAPIFactory
<ExtensionActionAPI
>*
125 ExtensionActionAPI::GetFactoryInstance() {
126 return g_factory
.Pointer();
130 ExtensionActionAPI
* ExtensionActionAPI::Get(content::BrowserContext
* context
) {
131 return BrowserContextKeyedAPIFactory
<ExtensionActionAPI
>::Get(context
);
134 void ExtensionActionAPI::AddObserver(Observer
* observer
) {
135 observers_
.AddObserver(observer
);
138 void ExtensionActionAPI::RemoveObserver(Observer
* observer
) {
139 observers_
.RemoveObserver(observer
);
142 bool ExtensionActionAPI::GetBrowserActionVisibility(
143 const std::string
& extension_id
) {
144 bool visible
= false;
145 ExtensionPrefs
* prefs
= GetExtensionPrefs();
146 if (!prefs
|| !prefs
->ReadPrefAsBoolean(extension_id
,
147 kBrowserActionVisible
,
154 void ExtensionActionAPI::SetBrowserActionVisibility(
155 const std::string
& extension_id
,
157 if (GetBrowserActionVisibility(extension_id
) == visible
)
160 GetExtensionPrefs()->UpdateExtensionPref(extension_id
,
161 kBrowserActionVisible
,
162 new base::FundamentalValue(visible
));
163 FOR_EACH_OBSERVER(Observer
, observers_
, OnExtensionActionVisibilityChanged(
164 extension_id
, visible
));
167 ExtensionAction::ShowAction
ExtensionActionAPI::ExecuteExtensionAction(
168 const Extension
* extension
,
170 bool grant_active_tab_permissions
) {
171 content::WebContents
* web_contents
=
172 browser
->tab_strip_model()->GetActiveWebContents();
174 return ExtensionAction::ACTION_NONE
;
176 int tab_id
= SessionTabHelper::IdForTab(web_contents
);
178 ActiveScriptController
* active_script_controller
=
179 ActiveScriptController::GetForWebContents(web_contents
);
180 bool has_pending_scripts
= false;
181 if (active_script_controller
&&
182 active_script_controller
->WantsToRun(extension
)) {
183 has_pending_scripts
= true;
186 // Grant active tab if appropriate.
187 if (grant_active_tab_permissions
) {
188 TabHelper::FromWebContents(web_contents
)->active_tab_permission_granter()->
189 GrantIfRequested(extension
);
192 // If this was a request to run a script, it will have been run once active
193 // tab was granted. Return without executing the action, since we should only
194 // run pending scripts OR the extension action, not both.
195 if (has_pending_scripts
)
196 return ExtensionAction::ACTION_NONE
;
198 ExtensionAction
* extension_action
=
199 ExtensionActionManager::Get(browser_context_
)->GetExtensionAction(
202 // Anything that gets here should have a page or browser action.
203 DCHECK(extension_action
);
204 if (!extension_action
->GetIsVisible(tab_id
))
205 return ExtensionAction::ACTION_NONE
;
207 if (extension_action
->HasPopup(tab_id
))
208 return ExtensionAction::ACTION_SHOW_POPUP
;
210 ExtensionActionExecuted(*extension_action
, web_contents
);
211 return ExtensionAction::ACTION_NONE
;
214 bool ExtensionActionAPI::ShowExtensionActionPopup(
215 const Extension
* extension
,
217 bool grant_active_tab_permissions
) {
218 ExtensionAction
* extension_action
=
219 ExtensionActionManager::Get(browser_context_
)->GetExtensionAction(
221 if (!extension_action
)
224 if (extension_action
->action_type() == ActionInfo::TYPE_PAGE
&&
225 !FeatureSwitch::extension_action_redesign()->IsEnabled()) {
226 // We show page actions in the location bar unless the new toolbar is
228 return browser
->window()->GetLocationBar()->ShowPageActionPopup(
229 extension
, grant_active_tab_permissions
);
231 return browser
->window()->GetToolbarActionsBar()->ShowToolbarActionPopup(
232 extension
->id(), grant_active_tab_permissions
);
235 bool ExtensionActionAPI::ExtensionWantsToRun(
236 const Extension
* extension
, content::WebContents
* web_contents
) {
237 // An extension wants to act if it has a visible page action on the given
239 ExtensionAction
* page_action
=
240 ExtensionActionManager::Get(browser_context_
)->GetPageAction(*extension
);
242 page_action
->GetIsVisible(SessionTabHelper::IdForTab(web_contents
)))
245 // ... Or if it has pending scripts that need approval for execution.
246 ActiveScriptController
* active_script_controller
=
247 ActiveScriptController::GetForWebContents(web_contents
);
248 if (active_script_controller
&&
249 active_script_controller
->WantsToRun(extension
))
255 void ExtensionActionAPI::NotifyChange(ExtensionAction
* extension_action
,
256 content::WebContents
* web_contents
,
257 content::BrowserContext
* context
) {
261 OnExtensionActionUpdated(extension_action
, web_contents
, context
));
263 if (extension_action
->action_type() == ActionInfo::TYPE_PAGE
)
264 NotifyPageActionsChanged(web_contents
);
267 void ExtensionActionAPI::ClearAllValuesForTab(
268 content::WebContents
* web_contents
) {
269 DCHECK(web_contents
);
270 int tab_id
= SessionTabHelper::IdForTab(web_contents
);
271 content::BrowserContext
* browser_context
= web_contents
->GetBrowserContext();
272 const ExtensionSet
& enabled_extensions
=
273 ExtensionRegistry::Get(browser_context_
)->enabled_extensions();
274 ExtensionActionManager
* action_manager
=
275 ExtensionActionManager::Get(browser_context_
);
277 for (ExtensionSet::const_iterator iter
= enabled_extensions
.begin();
278 iter
!= enabled_extensions
.end(); ++iter
) {
279 ExtensionAction
* extension_action
=
280 action_manager
->GetExtensionAction(**iter
);
281 if (extension_action
) {
282 extension_action
->ClearAllValuesForTab(tab_id
);
283 NotifyChange(extension_action
, web_contents
, browser_context
);
288 ExtensionPrefs
* ExtensionActionAPI::GetExtensionPrefs() {
289 // This lazy initialization is more than just an optimization, because it
290 // allows tests to associate a new ExtensionPrefs with the browser context
291 // before we access it.
292 if (!extension_prefs_
)
293 extension_prefs_
= ExtensionPrefs::Get(browser_context_
);
294 return extension_prefs_
;
297 void ExtensionActionAPI::DispatchEventToExtension(
298 content::BrowserContext
* context
,
299 const std::string
& extension_id
,
300 events::HistogramValue histogram_value
,
301 const std::string
& event_name
,
302 scoped_ptr
<base::ListValue
> event_args
) {
303 if (!EventRouter::Get(context
))
306 scoped_ptr
<Event
> event(
307 new Event(histogram_value
, event_name
, event_args
.Pass()));
308 event
->restrict_to_browser_context
= context
;
309 event
->user_gesture
= EventRouter::USER_GESTURE_ENABLED
;
310 EventRouter::Get(context
)
311 ->DispatchEventToExtension(extension_id
, event
.Pass());
314 void ExtensionActionAPI::ExtensionActionExecuted(
315 const ExtensionAction
& extension_action
,
316 WebContents
* web_contents
) {
317 events::HistogramValue histogram_value
= events::UNKNOWN
;
318 const char* event_name
= NULL
;
319 switch (extension_action
.action_type()) {
320 case ActionInfo::TYPE_BROWSER
:
321 histogram_value
= events::BROWSER_ACTION_ON_CLICKED
;
322 event_name
= "browserAction.onClicked";
324 case ActionInfo::TYPE_PAGE
:
325 histogram_value
= events::PAGE_ACTION_ON_CLICKED
;
326 event_name
= "pageAction.onClicked";
328 case ActionInfo::TYPE_SYSTEM_INDICATOR
:
329 // The System Indicator handles its own clicks.
335 scoped_ptr
<base::ListValue
> args(new base::ListValue());
336 base::DictionaryValue
* tab_value
=
337 ExtensionTabUtil::CreateTabValue(web_contents
);
338 args
->Append(tab_value
);
340 DispatchEventToExtension(web_contents
->GetBrowserContext(),
341 extension_action
.extension_id(), histogram_value
,
342 event_name
, args
.Pass());
346 void ExtensionActionAPI::NotifyPageActionsChanged(
347 content::WebContents
* web_contents
) {
348 Browser
* browser
= chrome::FindBrowserWithWebContents(web_contents
);
351 LocationBar
* location_bar
=
352 browser
->window() ? browser
->window()->GetLocationBar() : NULL
;
355 location_bar
->UpdatePageActions();
357 FOR_EACH_OBSERVER(Observer
, observers_
, OnPageActionsUpdated(web_contents
));
360 void ExtensionActionAPI::Shutdown() {
361 FOR_EACH_OBSERVER(Observer
, observers_
, OnExtensionActionAPIShuttingDown());
365 // ExtensionActionFunction
368 ExtensionActionFunction::ExtensionActionFunction()
370 tab_id_(ExtensionAction::kDefaultTabId
),
372 extension_action_(NULL
) {
375 ExtensionActionFunction::~ExtensionActionFunction() {
378 bool ExtensionActionFunction::RunSync() {
379 ExtensionActionManager
* manager
= ExtensionActionManager::Get(GetProfile());
380 if (base::StartsWith(name(), "systemIndicator.",
381 base::CompareCase::INSENSITIVE_ASCII
)) {
382 extension_action_
= manager
->GetSystemIndicator(*extension());
384 extension_action_
= manager
->GetBrowserAction(*extension());
385 if (!extension_action_
) {
386 extension_action_
= manager
->GetPageAction(*extension());
389 if (!extension_action_
) {
390 // TODO(kalman): ideally the browserAction/pageAction APIs wouldn't event
391 // exist for extensions that don't have one declared. This should come as
392 // part of the Feature system.
393 error_
= kNoExtensionActionError
;
397 // Populates the tab_id_ and details_ members.
398 EXTENSION_FUNCTION_VALIDATE(ExtractDataFromArguments());
400 // Find the WebContents that contains this tab id if one is required.
401 if (tab_id_
!= ExtensionAction::kDefaultTabId
) {
402 ExtensionTabUtil::GetTabById(tab_id_
,
410 error_
= ErrorUtils::FormatErrorMessage(
411 kNoTabError
, base::IntToString(tab_id_
));
415 // Only browser actions and system indicators have a default tabId.
416 ActionInfo::Type action_type
= extension_action_
->action_type();
417 EXTENSION_FUNCTION_VALIDATE(
418 action_type
== ActionInfo::TYPE_BROWSER
||
419 action_type
== ActionInfo::TYPE_SYSTEM_INDICATOR
);
421 return RunExtensionAction();
424 bool ExtensionActionFunction::ExtractDataFromArguments() {
425 // There may or may not be details (depends on the function).
426 // The tabId might appear in details (if it exists), as the first
427 // argument besides the action type (depends on the function), or be omitted
429 base::Value
* first_arg
= NULL
;
430 if (!args_
->Get(0, &first_arg
))
433 switch (first_arg
->GetType()) {
434 case base::Value::TYPE_INTEGER
:
435 CHECK(first_arg
->GetAsInteger(&tab_id_
));
438 case base::Value::TYPE_DICTIONARY
: {
439 // Found the details argument.
440 details_
= static_cast<base::DictionaryValue
*>(first_arg
);
441 // Still need to check for the tabId within details.
442 base::Value
* tab_id_value
= NULL
;
443 if (details_
->Get("tabId", &tab_id_value
)) {
444 switch (tab_id_value
->GetType()) {
445 case base::Value::TYPE_NULL
:
446 // OK; tabId is optional, leave it default.
448 case base::Value::TYPE_INTEGER
:
449 CHECK(tab_id_value
->GetAsInteger(&tab_id_
));
456 // Not found; tabId is optional, leave it default.
460 case base::Value::TYPE_NULL
:
461 // The tabId might be an optional argument.
471 void ExtensionActionFunction::NotifyChange() {
472 ExtensionActionAPI::Get(GetProfile())->NotifyChange(
473 extension_action_
, contents_
, GetProfile());
476 bool ExtensionActionFunction::SetVisible(bool visible
) {
477 if (extension_action_
->GetIsVisible(tab_id_
) == visible
)
479 extension_action_
->SetIsVisible(tab_id_
, visible
);
484 bool ExtensionActionShowFunction::RunExtensionAction() {
485 return SetVisible(true);
488 bool ExtensionActionHideFunction::RunExtensionAction() {
489 return SetVisible(false);
492 bool ExtensionActionSetIconFunction::RunExtensionAction() {
493 EXTENSION_FUNCTION_VALIDATE(details_
);
495 // setIcon can take a variant argument: either a dictionary of canvas
496 // ImageData, or an icon index.
497 base::DictionaryValue
* canvas_set
= NULL
;
499 if (details_
->GetDictionary("imageData", &canvas_set
)) {
502 EXTENSION_FUNCTION_VALIDATE(
503 ExtensionAction::ParseIconFromCanvasDictionary(*canvas_set
, &icon
));
505 extension_action_
->SetIcon(tab_id_
, gfx::Image(icon
));
506 } else if (details_
->GetInteger("iconIndex", &icon_index
)) {
507 // Obsolete argument: ignore it.
510 EXTENSION_FUNCTION_VALIDATE(false);
516 bool ExtensionActionSetTitleFunction::RunExtensionAction() {
517 EXTENSION_FUNCTION_VALIDATE(details_
);
519 EXTENSION_FUNCTION_VALIDATE(details_
->GetString("title", &title
));
520 extension_action_
->SetTitle(tab_id_
, title
);
525 bool ExtensionActionSetPopupFunction::RunExtensionAction() {
526 EXTENSION_FUNCTION_VALIDATE(details_
);
527 std::string popup_string
;
528 EXTENSION_FUNCTION_VALIDATE(details_
->GetString("popup", &popup_string
));
531 if (!popup_string
.empty())
532 popup_url
= extension()->GetResourceURL(popup_string
);
534 extension_action_
->SetPopupUrl(tab_id_
, popup_url
);
539 bool ExtensionActionSetBadgeTextFunction::RunExtensionAction() {
540 EXTENSION_FUNCTION_VALIDATE(details_
);
541 std::string badge_text
;
542 EXTENSION_FUNCTION_VALIDATE(details_
->GetString("text", &badge_text
));
543 extension_action_
->SetBadgeText(tab_id_
, badge_text
);
548 bool ExtensionActionSetBadgeBackgroundColorFunction::RunExtensionAction() {
549 EXTENSION_FUNCTION_VALIDATE(details_
);
550 base::Value
* color_value
= NULL
;
551 EXTENSION_FUNCTION_VALIDATE(details_
->Get("color", &color_value
));
553 if (color_value
->IsType(base::Value::TYPE_LIST
)) {
554 base::ListValue
* list
= NULL
;
555 EXTENSION_FUNCTION_VALIDATE(details_
->GetList("color", &list
));
556 EXTENSION_FUNCTION_VALIDATE(list
->GetSize() == 4);
558 int color_array
[4] = {0};
559 for (size_t i
= 0; i
< arraysize(color_array
); ++i
) {
560 EXTENSION_FUNCTION_VALIDATE(list
->GetInteger(i
, &color_array
[i
]));
563 color
= SkColorSetARGB(color_array
[3], color_array
[0],
564 color_array
[1], color_array
[2]);
565 } else if (color_value
->IsType(base::Value::TYPE_STRING
)) {
566 std::string color_string
;
567 EXTENSION_FUNCTION_VALIDATE(details_
->GetString("color", &color_string
));
568 if (!image_util::ParseCSSColorString(color_string
, &color
))
572 extension_action_
->SetBadgeBackgroundColor(tab_id_
, color
);
577 bool ExtensionActionGetTitleFunction::RunExtensionAction() {
578 SetResult(new base::StringValue(extension_action_
->GetTitle(tab_id_
)));
582 bool ExtensionActionGetPopupFunction::RunExtensionAction() {
584 new base::StringValue(extension_action_
->GetPopupUrl(tab_id_
).spec()));
588 bool ExtensionActionGetBadgeTextFunction::RunExtensionAction() {
589 SetResult(new base::StringValue(extension_action_
->GetBadgeText(tab_id_
)));
593 bool ExtensionActionGetBadgeBackgroundColorFunction::RunExtensionAction() {
594 base::ListValue
* list
= new base::ListValue();
595 SkColor color
= extension_action_
->GetBadgeBackgroundColor(tab_id_
);
597 new base::FundamentalValue(static_cast<int>(SkColorGetR(color
))));
599 new base::FundamentalValue(static_cast<int>(SkColorGetG(color
))));
601 new base::FundamentalValue(static_cast<int>(SkColorGetB(color
))));
603 new base::FundamentalValue(static_cast<int>(SkColorGetA(color
))));
608 BrowserActionOpenPopupFunction::BrowserActionOpenPopupFunction()
609 : response_sent_(false) {
612 bool BrowserActionOpenPopupFunction::RunAsync() {
613 // We only allow the popup in the active window.
614 Profile
* profile
= GetProfile();
615 Browser
* browser
= chrome::FindLastActiveWithProfile(
616 profile
, chrome::GetActiveDesktop());
617 // It's possible that the last active browser actually corresponds to the
618 // associated incognito profile, and this won't be returned by
619 // FindLastActiveWithProfile. If the browser we found isn't active and the
620 // extension can operate incognito, then check the last active incognito, too.
621 if ((!browser
|| !browser
->window()->IsActive()) &&
622 util::IsIncognitoEnabled(extension()->id(), profile
) &&
623 profile
->HasOffTheRecordProfile()) {
624 browser
= chrome::FindLastActiveWithProfile(
625 profile
->GetOffTheRecordProfile(), chrome::GetActiveDesktop());
628 // If there's no active browser, or the Toolbar isn't visible, abort.
629 // Otherwise, try to open a popup in the active browser.
630 // TODO(justinlin): Remove toolbar check when http://crbug.com/308645 is
633 !browser
->window()->IsActive() ||
634 !browser
->window()->IsToolbarVisible() ||
635 !ExtensionActionAPI::Get(GetProfile())->ShowExtensionActionPopup(
636 extension_
.get(), browser
, false)) {
637 error_
= kOpenPopupError
;
641 // Even if this is for an incognito window, we want to use the normal profile.
642 // If the extension is spanning, then extension hosts are created with the
643 // original profile, and if it's split, then we know the api call came from
644 // the right profile.
645 registrar_
.Add(this, NOTIFICATION_EXTENSION_HOST_DID_STOP_FIRST_LOAD
,
646 content::Source
<Profile
>(profile
));
648 // Set a timeout for waiting for the notification that the popup is loaded.
649 // Waiting is required so that the popup view can be retrieved by the custom
650 // bindings for the response callback. It's also needed to keep this function
651 // instance around until a notification is observed.
652 base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
654 base::Bind(&BrowserActionOpenPopupFunction::OpenPopupTimedOut
, this),
655 base::TimeDelta::FromSeconds(10));
659 void BrowserActionOpenPopupFunction::OpenPopupTimedOut() {
663 DVLOG(1) << "chrome.browserAction.openPopup did not show a popup.";
664 error_
= kOpenPopupError
;
666 response_sent_
= true;
669 void BrowserActionOpenPopupFunction::Observe(
671 const content::NotificationSource
& source
,
672 const content::NotificationDetails
& details
) {
673 DCHECK_EQ(NOTIFICATION_EXTENSION_HOST_DID_STOP_FIRST_LOAD
, type
);
677 ExtensionHost
* host
= content::Details
<ExtensionHost
>(details
).ptr();
678 if (host
->extension_host_type() != VIEW_TYPE_EXTENSION_POPUP
||
679 host
->extension()->id() != extension_
->id())
683 response_sent_
= true;
684 registrar_
.RemoveAll();
687 } // namespace extensions