• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4 
5 // Implements the Chrome Extensions Cookies API.
6 
7 #include "chrome/browser/extensions/api/cookies/cookies_api.h"
8 
9 #include <vector>
10 
11 #include "base/bind.h"
12 #include "base/json/json_writer.h"
13 #include "base/lazy_instance.h"
14 #include "base/memory/linked_ptr.h"
15 #include "base/memory/scoped_ptr.h"
16 #include "base/time/time.h"
17 #include "base/values.h"
18 #include "chrome/browser/chrome_notification_types.h"
19 #include "chrome/browser/extensions/api/cookies/cookies_api_constants.h"
20 #include "chrome/browser/extensions/api/cookies/cookies_helpers.h"
21 #include "chrome/browser/profiles/profile.h"
22 #include "chrome/browser/ui/browser.h"
23 #include "chrome/browser/ui/browser_iterator.h"
24 #include "chrome/common/extensions/api/cookies.h"
25 #include "content/public/browser/browser_thread.h"
26 #include "content/public/browser/notification_service.h"
27 #include "extensions/browser/event_router.h"
28 #include "extensions/common/error_utils.h"
29 #include "extensions/common/extension.h"
30 #include "extensions/common/permissions/permissions_data.h"
31 #include "net/cookies/canonical_cookie.h"
32 #include "net/cookies/cookie_constants.h"
33 #include "net/cookies/cookie_monster.h"
34 #include "net/url_request/url_request_context.h"
35 #include "net/url_request/url_request_context_getter.h"
36 
37 using content::BrowserThread;
38 using extensions::api::cookies::Cookie;
39 using extensions::api::cookies::CookieStore;
40 
41 namespace Get = extensions::api::cookies::Get;
42 namespace GetAll = extensions::api::cookies::GetAll;
43 namespace GetAllCookieStores = extensions::api::cookies::GetAllCookieStores;
44 namespace Remove = extensions::api::cookies::Remove;
45 namespace Set = extensions::api::cookies::Set;
46 
47 namespace extensions {
48 namespace cookies = api::cookies;
49 namespace keys = cookies_api_constants;
50 
51 namespace {
52 
ParseUrl(ChromeAsyncExtensionFunction * function,const std::string & url_string,GURL * url,bool check_host_permissions)53 bool ParseUrl(ChromeAsyncExtensionFunction* function,
54               const std::string& url_string,
55               GURL* url,
56               bool check_host_permissions) {
57   *url = GURL(url_string);
58   if (!url->is_valid()) {
59     function->SetError(
60         ErrorUtils::FormatErrorMessage(keys::kInvalidUrlError, url_string));
61     return false;
62   }
63   // Check against host permissions if needed.
64   if (check_host_permissions &&
65       !function->GetExtension()->permissions_data()->HasHostPermission(*url)) {
66     function->SetError(ErrorUtils::FormatErrorMessage(
67         keys::kNoHostPermissionsError, url->spec()));
68     return false;
69   }
70   return true;
71 }
72 
ParseStoreContext(ChromeAsyncExtensionFunction * function,std::string * store_id,net::URLRequestContextGetter ** context)73 bool ParseStoreContext(ChromeAsyncExtensionFunction* function,
74                        std::string* store_id,
75                        net::URLRequestContextGetter** context) {
76   DCHECK((context || store_id->empty()));
77   Profile* store_profile = NULL;
78   if (!store_id->empty()) {
79     store_profile = cookies_helpers::ChooseProfileFromStoreId(
80         *store_id, function->GetProfile(), function->include_incognito());
81     if (!store_profile) {
82       function->SetError(ErrorUtils::FormatErrorMessage(
83           keys::kInvalidStoreIdError, *store_id));
84       return false;
85     }
86   } else {
87     // The store ID was not specified; use the current execution context's
88     // cookie store by default.
89     // GetCurrentBrowser() already takes into account incognito settings.
90     Browser* current_browser = function->GetCurrentBrowser();
91     if (!current_browser) {
92       function->SetError(keys::kNoCookieStoreFoundError);
93       return false;
94     }
95     store_profile = current_browser->profile();
96     *store_id = cookies_helpers::GetStoreIdFromProfile(store_profile);
97   }
98 
99   if (context)
100     *context = store_profile->GetRequestContext();
101   DCHECK(context);
102 
103   return true;
104 }
105 
106 }  // namespace
107 
CookiesEventRouter(content::BrowserContext * context)108 CookiesEventRouter::CookiesEventRouter(content::BrowserContext* context)
109     : profile_(Profile::FromBrowserContext(context)) {
110   CHECK(registrar_.IsEmpty());
111   registrar_.Add(this,
112                  chrome::NOTIFICATION_COOKIE_CHANGED,
113                  content::NotificationService::AllBrowserContextsAndSources());
114 }
115 
~CookiesEventRouter()116 CookiesEventRouter::~CookiesEventRouter() {
117 }
118 
Observe(int type,const content::NotificationSource & source,const content::NotificationDetails & details)119 void CookiesEventRouter::Observe(
120     int type,
121     const content::NotificationSource& source,
122     const content::NotificationDetails& details) {
123   Profile* profile = content::Source<Profile>(source).ptr();
124   if (!profile_->IsSameProfile(profile))
125     return;
126 
127   switch (type) {
128     case chrome::NOTIFICATION_COOKIE_CHANGED:
129       CookieChanged(
130           profile,
131           content::Details<ChromeCookieDetails>(details).ptr());
132       break;
133 
134     default:
135       NOTREACHED();
136   }
137 }
138 
CookieChanged(Profile * profile,ChromeCookieDetails * details)139 void CookiesEventRouter::CookieChanged(
140     Profile* profile,
141     ChromeCookieDetails* details) {
142   scoped_ptr<base::ListValue> args(new base::ListValue());
143   base::DictionaryValue* dict = new base::DictionaryValue();
144   dict->SetBoolean(keys::kRemovedKey, details->removed);
145 
146   scoped_ptr<Cookie> cookie(
147       cookies_helpers::CreateCookie(*details->cookie,
148           cookies_helpers::GetStoreIdFromProfile(profile)));
149   dict->Set(keys::kCookieKey, cookie->ToValue().release());
150 
151   // Map the internal cause to an external string.
152   std::string cause;
153   switch (details->cause) {
154     case net::CookieMonster::Delegate::CHANGE_COOKIE_EXPLICIT:
155       cause = keys::kExplicitChangeCause;
156       break;
157 
158     case net::CookieMonster::Delegate::CHANGE_COOKIE_OVERWRITE:
159       cause = keys::kOverwriteChangeCause;
160       break;
161 
162     case net::CookieMonster::Delegate::CHANGE_COOKIE_EXPIRED:
163       cause = keys::kExpiredChangeCause;
164       break;
165 
166     case net::CookieMonster::Delegate::CHANGE_COOKIE_EVICTED:
167       cause = keys::kEvictedChangeCause;
168       break;
169 
170     case net::CookieMonster::Delegate::CHANGE_COOKIE_EXPIRED_OVERWRITE:
171       cause = keys::kExpiredOverwriteChangeCause;
172       break;
173 
174     default:
175       NOTREACHED();
176   }
177   dict->SetString(keys::kCauseKey, cause);
178 
179   args->Append(dict);
180 
181   GURL cookie_domain =
182       cookies_helpers::GetURLFromCanonicalCookie(*details->cookie);
183   DispatchEvent(profile,
184                 cookies::OnChanged::kEventName,
185                 args.Pass(),
186                 cookie_domain);
187 }
188 
DispatchEvent(content::BrowserContext * context,const std::string & event_name,scoped_ptr<base::ListValue> event_args,GURL & cookie_domain)189 void CookiesEventRouter::DispatchEvent(content::BrowserContext* context,
190                                        const std::string& event_name,
191                                        scoped_ptr<base::ListValue> event_args,
192                                        GURL& cookie_domain) {
193   EventRouter* router = context ? extensions::EventRouter::Get(context) : NULL;
194   if (!router)
195     return;
196   scoped_ptr<Event> event(new Event(event_name, event_args.Pass()));
197   event->restrict_to_browser_context = context;
198   event->event_url = cookie_domain;
199   router->BroadcastEvent(event.Pass());
200 }
201 
CookiesGetFunction()202 CookiesGetFunction::CookiesGetFunction() {
203 }
204 
~CookiesGetFunction()205 CookiesGetFunction::~CookiesGetFunction() {
206 }
207 
RunAsync()208 bool CookiesGetFunction::RunAsync() {
209   parsed_args_ = Get::Params::Create(*args_);
210   EXTENSION_FUNCTION_VALIDATE(parsed_args_.get());
211 
212   // Read/validate input parameters.
213   if (!ParseUrl(this, parsed_args_->details.url, &url_, true))
214     return false;
215 
216   std::string store_id =
217       parsed_args_->details.store_id.get() ? *parsed_args_->details.store_id
218                                            : std::string();
219   net::URLRequestContextGetter* store_context = NULL;
220   if (!ParseStoreContext(this, &store_id, &store_context))
221     return false;
222   store_browser_context_ = store_context;
223   if (!parsed_args_->details.store_id.get())
224     parsed_args_->details.store_id.reset(new std::string(store_id));
225 
226   store_browser_context_ = store_context;
227 
228   bool rv = BrowserThread::PostTask(
229       BrowserThread::IO, FROM_HERE,
230       base::Bind(&CookiesGetFunction::GetCookieOnIOThread, this));
231   DCHECK(rv);
232 
233   // Will finish asynchronously.
234   return true;
235 }
236 
GetCookieOnIOThread()237 void CookiesGetFunction::GetCookieOnIOThread() {
238   DCHECK_CURRENTLY_ON(BrowserThread::IO);
239   net::CookieStore* cookie_store =
240       store_browser_context_->GetURLRequestContext()->cookie_store();
241   cookies_helpers::GetCookieListFromStore(
242       cookie_store, url_,
243       base::Bind(&CookiesGetFunction::GetCookieCallback, this));
244 }
245 
GetCookieCallback(const net::CookieList & cookie_list)246 void CookiesGetFunction::GetCookieCallback(const net::CookieList& cookie_list) {
247   net::CookieList::const_iterator it;
248   for (it = cookie_list.begin(); it != cookie_list.end(); ++it) {
249     // Return the first matching cookie. Relies on the fact that the
250     // CookieMonster returns them in canonical order (longest path, then
251     // earliest creation time).
252     if (it->Name() == parsed_args_->details.name) {
253       scoped_ptr<Cookie> cookie(
254           cookies_helpers::CreateCookie(*it, *parsed_args_->details.store_id));
255       results_ = Get::Results::Create(*cookie);
256       break;
257     }
258   }
259 
260   // The cookie doesn't exist; return null.
261   if (it == cookie_list.end())
262     SetResult(base::Value::CreateNullValue());
263 
264   bool rv = BrowserThread::PostTask(
265       BrowserThread::UI, FROM_HERE,
266       base::Bind(&CookiesGetFunction::RespondOnUIThread, this));
267   DCHECK(rv);
268 }
269 
RespondOnUIThread()270 void CookiesGetFunction::RespondOnUIThread() {
271   DCHECK_CURRENTLY_ON(BrowserThread::UI);
272   SendResponse(true);
273 }
274 
CookiesGetAllFunction()275 CookiesGetAllFunction::CookiesGetAllFunction() {
276 }
277 
~CookiesGetAllFunction()278 CookiesGetAllFunction::~CookiesGetAllFunction() {
279 }
280 
RunAsync()281 bool CookiesGetAllFunction::RunAsync() {
282   parsed_args_ = GetAll::Params::Create(*args_);
283   EXTENSION_FUNCTION_VALIDATE(parsed_args_.get());
284 
285   if (parsed_args_->details.url.get() &&
286       !ParseUrl(this, *parsed_args_->details.url, &url_, false)) {
287     return false;
288   }
289 
290   std::string store_id =
291       parsed_args_->details.store_id.get() ? *parsed_args_->details.store_id
292                                            : std::string();
293   net::URLRequestContextGetter* store_context = NULL;
294   if (!ParseStoreContext(this, &store_id, &store_context))
295     return false;
296   store_browser_context_ = store_context;
297   if (!parsed_args_->details.store_id.get())
298     parsed_args_->details.store_id.reset(new std::string(store_id));
299 
300   bool rv = BrowserThread::PostTask(
301       BrowserThread::IO, FROM_HERE,
302       base::Bind(&CookiesGetAllFunction::GetAllCookiesOnIOThread, this));
303   DCHECK(rv);
304 
305   // Will finish asynchronously.
306   return true;
307 }
308 
GetAllCookiesOnIOThread()309 void CookiesGetAllFunction::GetAllCookiesOnIOThread() {
310   DCHECK_CURRENTLY_ON(BrowserThread::IO);
311   net::CookieStore* cookie_store =
312       store_browser_context_->GetURLRequestContext()->cookie_store();
313   cookies_helpers::GetCookieListFromStore(
314       cookie_store, url_,
315       base::Bind(&CookiesGetAllFunction::GetAllCookiesCallback, this));
316 }
317 
GetAllCookiesCallback(const net::CookieList & cookie_list)318 void CookiesGetAllFunction::GetAllCookiesCallback(
319     const net::CookieList& cookie_list) {
320   const extensions::Extension* extension = GetExtension();
321   if (extension) {
322     std::vector<linked_ptr<Cookie> > match_vector;
323     cookies_helpers::AppendMatchingCookiesToVector(
324         cookie_list, url_, &parsed_args_->details,
325         GetExtension(), &match_vector);
326 
327     results_ = GetAll::Results::Create(match_vector);
328   }
329   bool rv = BrowserThread::PostTask(
330       BrowserThread::UI, FROM_HERE,
331       base::Bind(&CookiesGetAllFunction::RespondOnUIThread, this));
332   DCHECK(rv);
333 }
334 
RespondOnUIThread()335 void CookiesGetAllFunction::RespondOnUIThread() {
336   DCHECK_CURRENTLY_ON(BrowserThread::UI);
337   SendResponse(true);
338 }
339 
CookiesSetFunction()340 CookiesSetFunction::CookiesSetFunction() : success_(false) {
341 }
342 
~CookiesSetFunction()343 CookiesSetFunction::~CookiesSetFunction() {
344 }
345 
RunAsync()346 bool CookiesSetFunction::RunAsync() {
347   parsed_args_ = Set::Params::Create(*args_);
348   EXTENSION_FUNCTION_VALIDATE(parsed_args_.get());
349 
350   // Read/validate input parameters.
351   if (!ParseUrl(this, parsed_args_->details.url, &url_, true))
352       return false;
353 
354   std::string store_id =
355       parsed_args_->details.store_id.get() ? *parsed_args_->details.store_id
356                                            : std::string();
357   net::URLRequestContextGetter* store_context = NULL;
358   if (!ParseStoreContext(this, &store_id, &store_context))
359     return false;
360   store_browser_context_ = store_context;
361   if (!parsed_args_->details.store_id.get())
362     parsed_args_->details.store_id.reset(new std::string(store_id));
363 
364   bool rv = BrowserThread::PostTask(
365       BrowserThread::IO, FROM_HERE,
366       base::Bind(&CookiesSetFunction::SetCookieOnIOThread, this));
367   DCHECK(rv);
368 
369   // Will finish asynchronously.
370   return true;
371 }
372 
SetCookieOnIOThread()373 void CookiesSetFunction::SetCookieOnIOThread() {
374   DCHECK_CURRENTLY_ON(BrowserThread::IO);
375   net::CookieMonster* cookie_monster =
376       store_browser_context_->GetURLRequestContext()
377           ->cookie_store()
378           ->GetCookieMonster();
379 
380   base::Time expiration_time;
381   if (parsed_args_->details.expiration_date.get()) {
382     // Time::FromDoubleT converts double time 0 to empty Time object. So we need
383     // to do special handling here.
384     expiration_time = (*parsed_args_->details.expiration_date == 0) ?
385         base::Time::UnixEpoch() :
386         base::Time::FromDoubleT(*parsed_args_->details.expiration_date);
387   }
388 
389   cookie_monster->SetCookieWithDetailsAsync(
390       url_,
391       parsed_args_->details.name.get() ? *parsed_args_->details.name
392                                        : std::string(),
393       parsed_args_->details.value.get() ? *parsed_args_->details.value
394                                         : std::string(),
395       parsed_args_->details.domain.get() ? *parsed_args_->details.domain
396                                          : std::string(),
397       parsed_args_->details.path.get() ? *parsed_args_->details.path
398                                        : std::string(),
399       expiration_time,
400       parsed_args_->details.secure.get() ? *parsed_args_->details.secure.get()
401                                          : false,
402       parsed_args_->details.http_only.get() ? *parsed_args_->details.http_only
403                                             : false,
404       net::COOKIE_PRIORITY_DEFAULT,
405       base::Bind(&CookiesSetFunction::PullCookie, this));
406 }
407 
PullCookie(bool set_cookie_result)408 void CookiesSetFunction::PullCookie(bool set_cookie_result) {
409   // Pull the newly set cookie.
410   net::CookieMonster* cookie_monster =
411       store_browser_context_->GetURLRequestContext()
412           ->cookie_store()
413           ->GetCookieMonster();
414   success_ = set_cookie_result;
415   cookies_helpers::GetCookieListFromStore(
416       cookie_monster, url_,
417       base::Bind(&CookiesSetFunction::PullCookieCallback, this));
418 }
419 
PullCookieCallback(const net::CookieList & cookie_list)420 void CookiesSetFunction::PullCookieCallback(
421     const net::CookieList& cookie_list) {
422   net::CookieList::const_iterator it;
423   for (it = cookie_list.begin(); it != cookie_list.end(); ++it) {
424     // Return the first matching cookie. Relies on the fact that the
425     // CookieMonster returns them in canonical order (longest path, then
426     // earliest creation time).
427     std::string name =
428         parsed_args_->details.name.get() ? *parsed_args_->details.name
429                                          : std::string();
430     if (it->Name() == name) {
431       scoped_ptr<Cookie> cookie(
432           cookies_helpers::CreateCookie(*it, *parsed_args_->details.store_id));
433       results_ = Set::Results::Create(*cookie);
434       break;
435     }
436   }
437 
438   bool rv = BrowserThread::PostTask(
439       BrowserThread::UI, FROM_HERE,
440       base::Bind(&CookiesSetFunction::RespondOnUIThread, this));
441   DCHECK(rv);
442 }
443 
RespondOnUIThread()444 void CookiesSetFunction::RespondOnUIThread() {
445   DCHECK_CURRENTLY_ON(BrowserThread::UI);
446   if (!success_) {
447     std::string name =
448         parsed_args_->details.name.get() ? *parsed_args_->details.name
449                                          : std::string();
450     error_ = ErrorUtils::FormatErrorMessage(keys::kCookieSetFailedError, name);
451   }
452   SendResponse(success_);
453 }
454 
CookiesRemoveFunction()455 CookiesRemoveFunction::CookiesRemoveFunction() {
456 }
457 
~CookiesRemoveFunction()458 CookiesRemoveFunction::~CookiesRemoveFunction() {
459 }
460 
RunAsync()461 bool CookiesRemoveFunction::RunAsync() {
462   parsed_args_ = Remove::Params::Create(*args_);
463   EXTENSION_FUNCTION_VALIDATE(parsed_args_.get());
464 
465   // Read/validate input parameters.
466   if (!ParseUrl(this, parsed_args_->details.url, &url_, true))
467     return false;
468 
469   std::string store_id =
470       parsed_args_->details.store_id.get() ? *parsed_args_->details.store_id
471                                            : std::string();
472   net::URLRequestContextGetter* store_context = NULL;
473   if (!ParseStoreContext(this, &store_id, &store_context))
474     return false;
475   store_browser_context_ = store_context;
476   if (!parsed_args_->details.store_id.get())
477     parsed_args_->details.store_id.reset(new std::string(store_id));
478 
479   // Pass the work off to the IO thread.
480   bool rv = BrowserThread::PostTask(
481       BrowserThread::IO, FROM_HERE,
482       base::Bind(&CookiesRemoveFunction::RemoveCookieOnIOThread, this));
483   DCHECK(rv);
484 
485   // Will return asynchronously.
486   return true;
487 }
488 
RemoveCookieOnIOThread()489 void CookiesRemoveFunction::RemoveCookieOnIOThread() {
490   DCHECK_CURRENTLY_ON(BrowserThread::IO);
491 
492   // Remove the cookie
493   net::CookieStore* cookie_store =
494       store_browser_context_->GetURLRequestContext()->cookie_store();
495   cookie_store->DeleteCookieAsync(
496       url_, parsed_args_->details.name,
497       base::Bind(&CookiesRemoveFunction::RemoveCookieCallback, this));
498 }
499 
RemoveCookieCallback()500 void CookiesRemoveFunction::RemoveCookieCallback() {
501   // Build the callback result
502   Remove::Results::Details details;
503   details.name = parsed_args_->details.name;
504   details.url = url_.spec();
505   details.store_id = *parsed_args_->details.store_id;
506   results_ = Remove::Results::Create(details);
507 
508   // Return to UI thread
509   bool rv = BrowserThread::PostTask(
510       BrowserThread::UI, FROM_HERE,
511       base::Bind(&CookiesRemoveFunction::RespondOnUIThread, this));
512   DCHECK(rv);
513 }
514 
RespondOnUIThread()515 void CookiesRemoveFunction::RespondOnUIThread() {
516   DCHECK_CURRENTLY_ON(BrowserThread::UI);
517   SendResponse(true);
518 }
519 
RunSync()520 bool CookiesGetAllCookieStoresFunction::RunSync() {
521   Profile* original_profile = GetProfile();
522   DCHECK(original_profile);
523   scoped_ptr<base::ListValue> original_tab_ids(new base::ListValue());
524   Profile* incognito_profile = NULL;
525   scoped_ptr<base::ListValue> incognito_tab_ids;
526   if (include_incognito() && GetProfile()->HasOffTheRecordProfile()) {
527     incognito_profile = GetProfile()->GetOffTheRecordProfile();
528     if (incognito_profile)
529       incognito_tab_ids.reset(new base::ListValue());
530   }
531   DCHECK(original_profile != incognito_profile);
532 
533   // Iterate through all browser instances, and for each browser,
534   // add its tab IDs to either the regular or incognito tab ID list depending
535   // whether the browser is regular or incognito.
536   for (chrome::BrowserIterator it; !it.done(); it.Next()) {
537     Browser* browser = *it;
538     if (browser->profile() == original_profile) {
539       cookies_helpers::AppendToTabIdList(browser, original_tab_ids.get());
540     } else if (incognito_tab_ids.get() &&
541                browser->profile() == incognito_profile) {
542       cookies_helpers::AppendToTabIdList(browser, incognito_tab_ids.get());
543     }
544   }
545   // Return a list of all cookie stores with at least one open tab.
546   std::vector<linked_ptr<CookieStore> > cookie_stores;
547   if (original_tab_ids->GetSize() > 0) {
548     cookie_stores.push_back(make_linked_ptr(
549         cookies_helpers::CreateCookieStore(
550             original_profile, original_tab_ids.release()).release()));
551   }
552   if (incognito_tab_ids.get() && incognito_tab_ids->GetSize() > 0 &&
553       incognito_profile) {
554     cookie_stores.push_back(make_linked_ptr(
555         cookies_helpers::CreateCookieStore(
556             incognito_profile, incognito_tab_ids.release()).release()));
557   }
558   results_ = GetAllCookieStores::Results::Create(cookie_stores);
559   return true;
560 }
561 
CookiesAPI(content::BrowserContext * context)562 CookiesAPI::CookiesAPI(content::BrowserContext* context)
563     : browser_context_(context) {
564   EventRouter::Get(browser_context_)
565       ->RegisterObserver(this, cookies::OnChanged::kEventName);
566 }
567 
~CookiesAPI()568 CookiesAPI::~CookiesAPI() {
569 }
570 
Shutdown()571 void CookiesAPI::Shutdown() {
572   EventRouter::Get(browser_context_)->UnregisterObserver(this);
573 }
574 
575 static base::LazyInstance<BrowserContextKeyedAPIFactory<CookiesAPI> >
576     g_factory = LAZY_INSTANCE_INITIALIZER;
577 
578 // static
GetFactoryInstance()579 BrowserContextKeyedAPIFactory<CookiesAPI>* CookiesAPI::GetFactoryInstance() {
580   return g_factory.Pointer();
581 }
582 
OnListenerAdded(const extensions::EventListenerInfo & details)583 void CookiesAPI::OnListenerAdded(
584     const extensions::EventListenerInfo& details) {
585   cookies_event_router_.reset(new CookiesEventRouter(browser_context_));
586   EventRouter::Get(browser_context_)->UnregisterObserver(this);
587 }
588 
589 }  // namespace extensions
590