1// Copyright 2013 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.
4
5#include "chrome/browser/extensions/api/tabs/tabs_event_router.h"
6
7#include "base/json/json_writer.h"
8#include "base/values.h"
9#include "chrome/browser/chrome_notification_types.h"
10#include "chrome/browser/extensions/api/tabs/tabs_constants.h"
11#include "chrome/browser/extensions/api/tabs/tabs_windows_api.h"
12#include "chrome/browser/extensions/api/tabs/windows_event_router.h"
13#include "chrome/browser/extensions/extension_tab_util.h"
14#include "chrome/browser/profiles/profile.h"
15#include "chrome/browser/ui/browser.h"
16#include "chrome/browser/ui/browser_iterator.h"
17#include "chrome/browser/ui/browser_list.h"
18#include "chrome/browser/ui/tabs/tab_strip_model.h"
19#include "chrome/common/extensions/extension_constants.h"
20#include "content/public/browser/favicon_status.h"
21#include "content/public/browser/navigation_controller.h"
22#include "content/public/browser/navigation_entry.h"
23#include "content/public/browser/notification_service.h"
24#include "content/public/browser/notification_types.h"
25#include "content/public/browser/web_contents.h"
26
27using base::DictionaryValue;
28using base::ListValue;
29using base::FundamentalValue;
30using content::NavigationController;
31using content::WebContents;
32
33namespace extensions {
34
35namespace {
36
37namespace tabs = api::tabs;
38
39void WillDispatchTabUpdatedEvent(
40    WebContents* contents,
41    const base::DictionaryValue* changed_properties,
42    content::BrowserContext* context,
43    const Extension* extension,
44    base::ListValue* event_args) {
45  // Overwrite the second argument with the appropriate properties dictionary,
46  // depending on extension permissions.
47  base::DictionaryValue* properties_value = changed_properties->DeepCopy();
48  ExtensionTabUtil::ScrubTabValueForExtension(contents,
49                                              extension,
50                                              properties_value);
51  event_args->Set(1, properties_value);
52
53  // Overwrite the third arg with our tab value as seen by this extension.
54  event_args->Set(2, ExtensionTabUtil::CreateTabValue(contents, extension));
55}
56
57}  // namespace
58
59TabsEventRouter::TabEntry::TabEntry() : complete_waiting_on_load_(false),
60                                        url_() {
61}
62
63base::DictionaryValue* TabsEventRouter::TabEntry::UpdateLoadState(
64    const WebContents* contents) {
65  // The tab may go in & out of loading (for instance if iframes navigate).
66  // We only want to respond to the first change from loading to !loading after
67  // the NAV_ENTRY_COMMITTED was fired.
68  if (!complete_waiting_on_load_ || contents->IsLoading())
69    return NULL;
70
71  // Send "complete" state change.
72  complete_waiting_on_load_ = false;
73  base::DictionaryValue* changed_properties = new base::DictionaryValue();
74  changed_properties->SetString(tabs_constants::kStatusKey,
75                                tabs_constants::kStatusValueComplete);
76  return changed_properties;
77}
78
79base::DictionaryValue* TabsEventRouter::TabEntry::DidNavigate(
80    const WebContents* contents) {
81  // Send "loading" state change.
82  complete_waiting_on_load_ = true;
83  base::DictionaryValue* changed_properties = new base::DictionaryValue();
84  changed_properties->SetString(tabs_constants::kStatusKey,
85                                tabs_constants::kStatusValueLoading);
86
87  if (contents->GetURL() != url_) {
88    url_ = contents->GetURL();
89    changed_properties->SetString(tabs_constants::kUrlKey, url_.spec());
90  }
91
92  return changed_properties;
93}
94
95TabsEventRouter::TabsEventRouter(Profile* profile) : profile_(profile) {
96  DCHECK(!profile->IsOffTheRecord());
97
98  BrowserList::AddObserver(this);
99
100  // Init() can happen after the browser is running, so catch up with any
101  // windows that already exist.
102  for (chrome::BrowserIterator it; !it.done(); it.Next()) {
103    RegisterForBrowserNotifications(*it);
104
105    // Also catch up our internal bookkeeping of tab entries.
106    Browser* browser = *it;
107    if (browser->tab_strip_model()) {
108      for (int i = 0; i < browser->tab_strip_model()->count(); ++i) {
109        WebContents* contents = browser->tab_strip_model()->GetWebContentsAt(i);
110        int tab_id = ExtensionTabUtil::GetTabId(contents);
111        tab_entries_[tab_id] = TabEntry();
112      }
113    }
114  }
115}
116
117TabsEventRouter::~TabsEventRouter() {
118  BrowserList::RemoveObserver(this);
119}
120
121void TabsEventRouter::OnBrowserAdded(Browser* browser) {
122  RegisterForBrowserNotifications(browser);
123}
124
125void TabsEventRouter::RegisterForBrowserNotifications(Browser* browser) {
126  if (!profile_->IsSameProfile(browser->profile()))
127    return;
128  // Start listening to TabStripModel events for this browser.
129  TabStripModel* tab_strip = browser->tab_strip_model();
130  tab_strip->AddObserver(this);
131
132  for (int i = 0; i < tab_strip->count(); ++i) {
133    RegisterForTabNotifications(tab_strip->GetWebContentsAt(i));
134  }
135}
136
137void TabsEventRouter::RegisterForTabNotifications(WebContents* contents) {
138  registrar_.Add(
139      this, content::NOTIFICATION_NAV_ENTRY_COMMITTED,
140      content::Source<NavigationController>(&contents->GetController()));
141
142  // Observing NOTIFICATION_WEB_CONTENTS_DESTROYED is necessary because it's
143  // possible for tabs to be created, detached and then destroyed without
144  // ever having been re-attached and closed. This happens in the case of
145  // a devtools WebContents that is opened in window, docked, then closed.
146  registrar_.Add(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED,
147                 content::Source<WebContents>(contents));
148
149  registrar_.Add(this, chrome::NOTIFICATION_FAVICON_UPDATED,
150                 content::Source<WebContents>(contents));
151
152  ZoomController::FromWebContents(contents)->AddObserver(this);
153}
154
155void TabsEventRouter::UnregisterForTabNotifications(WebContents* contents) {
156  registrar_.Remove(this, content::NOTIFICATION_NAV_ENTRY_COMMITTED,
157      content::Source<NavigationController>(&contents->GetController()));
158  registrar_.Remove(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED,
159      content::Source<WebContents>(contents));
160  registrar_.Remove(this, chrome::NOTIFICATION_FAVICON_UPDATED,
161      content::Source<WebContents>(contents));
162
163  ZoomController::FromWebContents(contents)->RemoveObserver(this);
164}
165
166void TabsEventRouter::OnBrowserRemoved(Browser* browser) {
167  if (!profile_->IsSameProfile(browser->profile()))
168    return;
169
170  // Stop listening to TabStripModel events for this browser.
171  browser->tab_strip_model()->RemoveObserver(this);
172}
173
174void TabsEventRouter::OnBrowserSetLastActive(Browser* browser) {
175  TabsWindowsAPI* tabs_window_api = TabsWindowsAPI::Get(profile_);
176  if (tabs_window_api) {
177    tabs_window_api->windows_event_router()->OnActiveWindowChanged(
178        browser ? browser->extension_window_controller() : NULL);
179  }
180}
181
182static void WillDispatchTabCreatedEvent(WebContents* contents,
183                                        bool active,
184                                        content::BrowserContext* context,
185                                        const Extension* extension,
186                                        base::ListValue* event_args) {
187  base::DictionaryValue* tab_value = ExtensionTabUtil::CreateTabValue(
188      contents, extension);
189  event_args->Clear();
190  event_args->Append(tab_value);
191  tab_value->SetBoolean(tabs_constants::kSelectedKey, active);
192}
193
194void TabsEventRouter::TabCreatedAt(WebContents* contents,
195                                   int index,
196                                   bool active) {
197  Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
198  scoped_ptr<base::ListValue> args(new base::ListValue);
199  scoped_ptr<Event> event(new Event(tabs::OnCreated::kEventName, args.Pass()));
200  event->restrict_to_browser_context = profile;
201  event->user_gesture = EventRouter::USER_GESTURE_NOT_ENABLED;
202  event->will_dispatch_callback =
203      base::Bind(&WillDispatchTabCreatedEvent, contents, active);
204  EventRouter::Get(profile)->BroadcastEvent(event.Pass());
205
206  RegisterForTabNotifications(contents);
207}
208
209void TabsEventRouter::TabInsertedAt(WebContents* contents,
210                                    int index,
211                                    bool active) {
212  // If tab is new, send created event.
213  int tab_id = ExtensionTabUtil::GetTabId(contents);
214  if (!GetTabEntry(contents)) {
215    tab_entries_[tab_id] = TabEntry();
216
217    TabCreatedAt(contents, index, active);
218    return;
219  }
220
221  scoped_ptr<base::ListValue> args(new base::ListValue);
222  args->Append(new FundamentalValue(tab_id));
223
224  base::DictionaryValue* object_args = new base::DictionaryValue();
225  object_args->Set(tabs_constants::kNewWindowIdKey,
226                   new FundamentalValue(
227                       ExtensionTabUtil::GetWindowIdOfTab(contents)));
228  object_args->Set(tabs_constants::kNewPositionKey,
229                   new FundamentalValue(index));
230  args->Append(object_args);
231
232  Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
233  DispatchEvent(profile, tabs::OnAttached::kEventName, args.Pass(),
234                EventRouter::USER_GESTURE_UNKNOWN);
235}
236
237void TabsEventRouter::TabDetachedAt(WebContents* contents, int index) {
238  if (!GetTabEntry(contents)) {
239    // The tab was removed. Don't send detach event.
240    return;
241  }
242
243  scoped_ptr<base::ListValue> args(new base::ListValue);
244  args->Append(
245      new FundamentalValue(ExtensionTabUtil::GetTabId(contents)));
246
247  base::DictionaryValue* object_args = new base::DictionaryValue();
248  object_args->Set(tabs_constants::kOldWindowIdKey,
249                   new FundamentalValue(
250                       ExtensionTabUtil::GetWindowIdOfTab(contents)));
251  object_args->Set(tabs_constants::kOldPositionKey,
252                   new FundamentalValue(index));
253  args->Append(object_args);
254
255  Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
256  DispatchEvent(profile,
257                tabs::OnDetached::kEventName,
258                args.Pass(),
259                EventRouter::USER_GESTURE_UNKNOWN);
260}
261
262void TabsEventRouter::TabClosingAt(TabStripModel* tab_strip_model,
263                                   WebContents* contents,
264                                   int index) {
265  int tab_id = ExtensionTabUtil::GetTabId(contents);
266
267  scoped_ptr<base::ListValue> args(new base::ListValue);
268  args->Append(new FundamentalValue(tab_id));
269
270  base::DictionaryValue* object_args = new base::DictionaryValue();
271  object_args->SetInteger(tabs_constants::kWindowIdKey,
272                          ExtensionTabUtil::GetWindowIdOfTab(contents));
273  object_args->SetBoolean(tabs_constants::kWindowClosing,
274                          tab_strip_model->closing_all());
275  args->Append(object_args);
276
277  Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
278  DispatchEvent(profile,
279                tabs::OnRemoved::kEventName,
280                args.Pass(),
281                EventRouter::USER_GESTURE_UNKNOWN);
282
283  int removed_count = tab_entries_.erase(tab_id);
284  DCHECK_GT(removed_count, 0);
285
286  UnregisterForTabNotifications(contents);
287}
288
289void TabsEventRouter::ActiveTabChanged(WebContents* old_contents,
290                                       WebContents* new_contents,
291                                       int index,
292                                       int reason) {
293  scoped_ptr<base::ListValue> args(new base::ListValue);
294  int tab_id = ExtensionTabUtil::GetTabId(new_contents);
295  args->Append(new FundamentalValue(tab_id));
296
297  base::DictionaryValue* object_args = new base::DictionaryValue();
298  object_args->Set(tabs_constants::kWindowIdKey,
299                   new FundamentalValue(
300                       ExtensionTabUtil::GetWindowIdOfTab(new_contents)));
301  args->Append(object_args);
302
303  // The onActivated event replaced onActiveChanged and onSelectionChanged. The
304  // deprecated events take two arguments: tabId, {windowId}.
305  Profile* profile =
306      Profile::FromBrowserContext(new_contents->GetBrowserContext());
307  EventRouter::UserGestureState gesture =
308      reason & CHANGE_REASON_USER_GESTURE
309      ? EventRouter::USER_GESTURE_ENABLED
310      : EventRouter::USER_GESTURE_NOT_ENABLED;
311  DispatchEvent(profile,
312                tabs::OnSelectionChanged::kEventName,
313                scoped_ptr<base::ListValue>(args->DeepCopy()),
314                gesture);
315  DispatchEvent(profile,
316                tabs::OnActiveChanged::kEventName,
317                scoped_ptr<base::ListValue>(args->DeepCopy()),
318                gesture);
319
320  // The onActivated event takes one argument: {windowId, tabId}.
321  args->Remove(0, NULL);
322  object_args->Set(tabs_constants::kTabIdKey,
323                   new FundamentalValue(tab_id));
324  DispatchEvent(profile, tabs::OnActivated::kEventName, args.Pass(), gesture);
325}
326
327void TabsEventRouter::TabSelectionChanged(
328    TabStripModel* tab_strip_model,
329    const ui::ListSelectionModel& old_model) {
330  ui::ListSelectionModel::SelectedIndices new_selection =
331      tab_strip_model->selection_model().selected_indices();
332  scoped_ptr<base::ListValue> all_tabs(new base::ListValue);
333
334  for (size_t i = 0; i < new_selection.size(); ++i) {
335    int index = new_selection[i];
336    WebContents* contents = tab_strip_model->GetWebContentsAt(index);
337    if (!contents)
338      break;
339    int tab_id = ExtensionTabUtil::GetTabId(contents);
340    all_tabs->Append(new FundamentalValue(tab_id));
341  }
342
343  scoped_ptr<base::ListValue> args(new base::ListValue);
344  scoped_ptr<base::DictionaryValue> select_info(new base::DictionaryValue);
345
346  select_info->Set(
347      tabs_constants::kWindowIdKey,
348      new FundamentalValue(
349          ExtensionTabUtil::GetWindowIdOfTabStripModel(tab_strip_model)));
350
351  select_info->Set(tabs_constants::kTabIdsKey, all_tabs.release());
352  args->Append(select_info.release());
353
354  // The onHighlighted event replaced onHighlightChanged.
355  Profile* profile = tab_strip_model->profile();
356  DispatchEvent(profile,
357                tabs::OnHighlightChanged::kEventName,
358                scoped_ptr<base::ListValue>(args->DeepCopy()),
359                EventRouter::USER_GESTURE_UNKNOWN);
360  DispatchEvent(profile,
361                tabs::OnHighlighted::kEventName,
362                args.Pass(),
363                EventRouter::USER_GESTURE_UNKNOWN);
364}
365
366void TabsEventRouter::TabMoved(WebContents* contents,
367                               int from_index,
368                               int to_index) {
369  scoped_ptr<base::ListValue> args(new base::ListValue);
370  args->Append(
371      new FundamentalValue(ExtensionTabUtil::GetTabId(contents)));
372
373  base::DictionaryValue* object_args = new base::DictionaryValue();
374  object_args->Set(tabs_constants::kWindowIdKey,
375                   new FundamentalValue(
376                       ExtensionTabUtil::GetWindowIdOfTab(contents)));
377  object_args->Set(tabs_constants::kFromIndexKey,
378                   new FundamentalValue(from_index));
379  object_args->Set(tabs_constants::kToIndexKey,
380                   new FundamentalValue(to_index));
381  args->Append(object_args);
382
383  Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
384  DispatchEvent(profile,
385                tabs::OnMoved::kEventName,
386                args.Pass(),
387                EventRouter::USER_GESTURE_UNKNOWN);
388}
389
390void TabsEventRouter::TabUpdated(WebContents* contents, bool did_navigate) {
391  TabEntry* entry = GetTabEntry(contents);
392  scoped_ptr<base::DictionaryValue> changed_properties;
393
394  CHECK(entry);
395
396  if (did_navigate)
397    changed_properties.reset(entry->DidNavigate(contents));
398  else
399    changed_properties.reset(entry->UpdateLoadState(contents));
400
401  if (changed_properties)
402    DispatchTabUpdatedEvent(contents, changed_properties.Pass());
403}
404
405void TabsEventRouter::FaviconUrlUpdated(WebContents* contents) {
406    content::NavigationEntry* entry =
407        contents->GetController().GetVisibleEntry();
408    if (!entry || !entry->GetFavicon().valid)
409      return;
410    scoped_ptr<base::DictionaryValue> changed_properties(
411        new base::DictionaryValue);
412    changed_properties->SetString(
413        tabs_constants::kFaviconUrlKey,
414        entry->GetFavicon().url.possibly_invalid_spec());
415    DispatchTabUpdatedEvent(contents, changed_properties.Pass());
416}
417
418void TabsEventRouter::DispatchEvent(
419    Profile* profile,
420    const std::string& event_name,
421    scoped_ptr<base::ListValue> args,
422    EventRouter::UserGestureState user_gesture) {
423  EventRouter* event_router = EventRouter::Get(profile);
424  if (!profile_->IsSameProfile(profile) || !event_router)
425    return;
426
427  scoped_ptr<Event> event(new Event(event_name, args.Pass()));
428  event->restrict_to_browser_context = profile;
429  event->user_gesture = user_gesture;
430  event_router->BroadcastEvent(event.Pass());
431}
432
433void TabsEventRouter::DispatchSimpleBrowserEvent(
434    Profile* profile, const int window_id, const std::string& event_name) {
435  if (!profile_->IsSameProfile(profile))
436    return;
437
438  scoped_ptr<base::ListValue> args(new base::ListValue);
439  args->Append(new FundamentalValue(window_id));
440
441  DispatchEvent(profile,
442                event_name,
443                args.Pass(),
444                EventRouter::USER_GESTURE_UNKNOWN);
445}
446
447void TabsEventRouter::DispatchTabUpdatedEvent(
448    WebContents* contents,
449    scoped_ptr<base::DictionaryValue> changed_properties) {
450  DCHECK(changed_properties);
451  DCHECK(contents);
452
453  // The state of the tab (as seen from the extension point of view) has
454  // changed.  Send a notification to the extension.
455  scoped_ptr<base::ListValue> args_base(new base::ListValue);
456
457  // First arg: The id of the tab that changed.
458  args_base->AppendInteger(ExtensionTabUtil::GetTabId(contents));
459
460  // Second arg: An object containing the changes to the tab state.  Filled in
461  // by WillDispatchTabUpdatedEvent as a copy of changed_properties, if the
462  // extension has the tabs permission.
463
464  // Third arg: An object containing the state of the tab. Filled in by
465  // WillDispatchTabUpdatedEvent.
466  Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
467
468  scoped_ptr<Event> event(
469      new Event(tabs::OnUpdated::kEventName, args_base.Pass()));
470  event->restrict_to_browser_context = profile;
471  event->user_gesture = EventRouter::USER_GESTURE_NOT_ENABLED;
472  event->will_dispatch_callback =
473      base::Bind(&WillDispatchTabUpdatedEvent,
474                 contents,
475                 changed_properties.get());
476  EventRouter::Get(profile)->BroadcastEvent(event.Pass());
477}
478
479TabsEventRouter::TabEntry* TabsEventRouter::GetTabEntry(WebContents* contents) {
480  int tab_id = ExtensionTabUtil::GetTabId(contents);
481  std::map<int, TabEntry>::iterator i = tab_entries_.find(tab_id);
482  if (tab_entries_.end() == i)
483    return NULL;
484  return &i->second;
485}
486
487void TabsEventRouter::Observe(int type,
488                              const content::NotificationSource& source,
489                              const content::NotificationDetails& details) {
490  if (type == content::NOTIFICATION_NAV_ENTRY_COMMITTED) {
491    NavigationController* source_controller =
492        content::Source<NavigationController>(source).ptr();
493    TabUpdated(source_controller->GetWebContents(), true);
494  } else if (type == content::NOTIFICATION_WEB_CONTENTS_DESTROYED) {
495    // Tab was destroyed after being detached (without being re-attached).
496    WebContents* contents = content::Source<WebContents>(source).ptr();
497    registrar_.Remove(this, content::NOTIFICATION_NAV_ENTRY_COMMITTED,
498        content::Source<NavigationController>(&contents->GetController()));
499    registrar_.Remove(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED,
500        content::Source<WebContents>(contents));
501    registrar_.Remove(this, chrome::NOTIFICATION_FAVICON_UPDATED,
502        content::Source<WebContents>(contents));
503  } else if (type == chrome::NOTIFICATION_FAVICON_UPDATED) {
504    bool icon_url_changed = *content::Details<bool>(details).ptr();
505    if (icon_url_changed)
506      FaviconUrlUpdated(content::Source<WebContents>(source).ptr());
507  } else {
508    NOTREACHED();
509  }
510}
511
512void TabsEventRouter::TabChangedAt(WebContents* contents,
513                                   int index,
514                                   TabChangeType change_type) {
515  TabUpdated(contents, false);
516}
517
518void TabsEventRouter::TabReplacedAt(TabStripModel* tab_strip_model,
519                                    WebContents* old_contents,
520                                    WebContents* new_contents,
521                                    int index) {
522  // Notify listeners that the next tabs closing or being added are due to
523  // WebContents being swapped.
524  const int new_tab_id = ExtensionTabUtil::GetTabId(new_contents);
525  const int old_tab_id = ExtensionTabUtil::GetTabId(old_contents);
526  scoped_ptr<base::ListValue> args(new base::ListValue);
527  args->Append(new FundamentalValue(new_tab_id));
528  args->Append(new FundamentalValue(old_tab_id));
529
530  DispatchEvent(Profile::FromBrowserContext(new_contents->GetBrowserContext()),
531                tabs::OnReplaced::kEventName,
532                args.Pass(),
533                EventRouter::USER_GESTURE_UNKNOWN);
534
535  // Update tab_entries_.
536  const int removed_count = tab_entries_.erase(old_tab_id);
537  DCHECK_GT(removed_count, 0);
538  UnregisterForTabNotifications(old_contents);
539
540  if (!GetTabEntry(new_contents)) {
541    tab_entries_[new_tab_id] = TabEntry();
542    RegisterForTabNotifications(new_contents);
543  }
544}
545
546void TabsEventRouter::TabPinnedStateChanged(WebContents* contents, int index) {
547  TabStripModel* tab_strip = NULL;
548  int tab_index;
549
550  if (ExtensionTabUtil::GetTabStripModel(contents, &tab_strip, &tab_index)) {
551    scoped_ptr<base::DictionaryValue> changed_properties(
552        new base::DictionaryValue());
553    changed_properties->SetBoolean(tabs_constants::kPinnedKey,
554                                   tab_strip->IsTabPinned(tab_index));
555    DispatchTabUpdatedEvent(contents, changed_properties.Pass());
556  }
557}
558
559void TabsEventRouter::OnZoomChanged(
560    const ZoomController::ZoomChangedEventData& data) {
561  DCHECK(data.web_contents);
562  int tab_id = ExtensionTabUtil::GetTabId(data.web_contents);
563  if (tab_id < 0)
564    return;
565
566  // Prepare the zoom change information.
567  api::tabs::OnZoomChange::ZoomChangeInfo zoom_change_info;
568  zoom_change_info.tab_id = tab_id;
569  zoom_change_info.old_zoom_factor =
570      content::ZoomLevelToZoomFactor(data.old_zoom_level);
571  zoom_change_info.new_zoom_factor =
572      content::ZoomLevelToZoomFactor(data.new_zoom_level);
573  ZoomModeToZoomSettings(data.zoom_mode,
574                         &zoom_change_info.zoom_settings);
575
576  // Dispatch the |onZoomChange| event.
577  Profile* profile = Profile::FromBrowserContext(
578      data.web_contents->GetBrowserContext());
579  DispatchEvent(profile,
580                tabs::OnZoomChange::kEventName,
581                api::tabs::OnZoomChange::Create(zoom_change_info),
582                EventRouter::USER_GESTURE_UNKNOWN);
583}
584
585}  // namespace extensions
586