Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "chrome/browser/extensions/api/identity/identity_api.h" | 5 #include "chrome/browser/extensions/api/identity/identity_api.h" |
| 6 | 6 |
| 7 #include "base/lazy_instance.h" | 7 #include "base/lazy_instance.h" |
| 8 #include "base/values.h" | 8 #include "base/values.h" |
| 9 #include "chrome/browser/extensions/extension_function_dispatcher.h" | 9 #include "chrome/browser/extensions/extension_function_dispatcher.h" |
| 10 #include "chrome/browser/extensions/extension_install_prompt.h" | 10 #include "chrome/browser/extensions/extension_install_prompt.h" |
| 11 #include "chrome/browser/extensions/extension_service.h" | 11 #include "chrome/browser/extensions/extension_service.h" |
| 12 #include "chrome/browser/extensions/permissions_updater.h" | 12 #include "chrome/browser/extensions/permissions_updater.h" |
| 13 #include "chrome/browser/profiles/profile.h" | 13 #include "chrome/browser/profiles/profile.h" |
| 14 #include "chrome/browser/signin/signin_manager.h" | |
| 15 #include "chrome/browser/signin/signin_manager_factory.h" | |
| 14 #include "chrome/browser/signin/token_service.h" | 16 #include "chrome/browser/signin/token_service.h" |
| 15 #include "chrome/browser/signin/token_service_factory.h" | 17 #include "chrome/browser/signin/token_service_factory.h" |
| 16 #include "chrome/browser/ui/browser.h" | 18 #include "chrome/browser/ui/browser.h" |
| 17 #include "chrome/browser/ui/browser_navigator.h" | |
| 18 #include "chrome/browser/ui/webui/signin/login_ui_service.h" | |
| 19 #include "chrome/browser/ui/webui/signin/login_ui_service_factory.h" | |
| 20 #include "chrome/browser/ui/webui/sync_promo/sync_promo_ui.h" | |
| 21 #include "chrome/common/extensions/api/experimental_identity.h" | 19 #include "chrome/common/extensions/api/experimental_identity.h" |
| 22 #include "chrome/common/extensions/api/identity/oauth2_manifest_handler.h" | 20 #include "chrome/common/extensions/api/identity/oauth2_manifest_handler.h" |
| 23 #include "chrome/common/extensions/extension.h" | 21 #include "chrome/common/extensions/extension.h" |
| 24 #include "chrome/common/extensions/extension_manifest_constants.h" | 22 #include "chrome/common/extensions/extension_manifest_constants.h" |
| 25 #include "chrome/common/extensions/manifest_handler.h" | 23 #include "chrome/common/extensions/manifest_handler.h" |
| 26 #include "chrome/common/url_constants.h" | 24 #include "chrome/common/url_constants.h" |
| 27 #include "content/public/common/page_transition_types.h" | 25 #include "content/public/common/page_transition_types.h" |
| 26 #include "google_apis/gaia/gaia_constants.h" | |
| 28 #include "googleurl/src/gurl.h" | 27 #include "googleurl/src/gurl.h" |
| 29 #include "ui/base/window_open_disposition.h" | 28 #include "ui/base/window_open_disposition.h" |
| 30 | 29 |
| 31 namespace extensions { | 30 namespace extensions { |
| 32 | 31 |
| 33 namespace identity_constants { | 32 namespace identity_constants { |
| 34 const char kInvalidClientId[] = "Invalid OAuth2 Client ID."; | 33 const char kInvalidClientId[] = "Invalid OAuth2 Client ID."; |
| 35 const char kInvalidScopes[] = "Invalid OAuth2 scopes."; | 34 const char kInvalidScopes[] = "Invalid OAuth2 scopes."; |
| 36 const char kAuthFailure[] = "OAuth2 request failed: "; | 35 const char kAuthFailure[] = "OAuth2 request failed: "; |
| 37 const char kNoGrant[] = "OAuth2 not granted or revoked."; | 36 const char kNoGrant[] = "OAuth2 not granted or revoked."; |
| 38 const char kUserRejected[] = "The user did not approve access."; | 37 const char kUserRejected[] = "The user did not approve access."; |
| 39 const char kUserNotSignedIn[] = "The user is not signed in."; | 38 const char kUserNotSignedIn[] = "The user is not signed in."; |
| 40 const char kInvalidRedirect[] = "Did not redirect to the right URL."; | 39 const char kInvalidRedirect[] = "Did not redirect to the right URL."; |
| 41 } // namespace identity_constants | 40 } // namespace identity_constants |
| 42 | 41 |
| 43 namespace GetAuthToken = api::experimental_identity::GetAuthToken; | 42 namespace GetAuthToken = api::experimental_identity::GetAuthToken; |
| 44 namespace LaunchWebAuthFlow = api::experimental_identity::LaunchWebAuthFlow; | 43 namespace LaunchWebAuthFlow = api::experimental_identity::LaunchWebAuthFlow; |
| 45 namespace identity = api::experimental_identity; | 44 namespace identity = api::experimental_identity; |
| 46 | 45 |
| 47 IdentityGetAuthTokenFunction::IdentityGetAuthTokenFunction() | 46 IdentityGetAuthTokenFunction::IdentityGetAuthTokenFunction() |
| 48 : interactive_(false) {} | 47 : should_prompt_for_scopes_(false), |
| 48 should_prompt_for_signin_(false) {} | |
| 49 IdentityGetAuthTokenFunction::~IdentityGetAuthTokenFunction() {} | 49 IdentityGetAuthTokenFunction::~IdentityGetAuthTokenFunction() {} |
| 50 | 50 |
| 51 bool IdentityGetAuthTokenFunction::RunImpl() { | 51 bool IdentityGetAuthTokenFunction::RunImpl() { |
| 52 scoped_ptr<GetAuthToken::Params> params(GetAuthToken::Params::Create(*args_)); | 52 scoped_ptr<GetAuthToken::Params> params(GetAuthToken::Params::Create(*args_)); |
| 53 EXTENSION_FUNCTION_VALIDATE(params.get()); | 53 EXTENSION_FUNCTION_VALIDATE(params.get()); |
| 54 if (params->details.get() && params->details->interactive.get()) | 54 bool interactive = params->details.get() && |
| 55 interactive_ = *params->details->interactive; | 55 params->details->interactive.get() && |
| 56 *params->details->interactive; | |
| 57 | |
| 58 should_prompt_for_scopes_ = interactive; | |
| 59 should_prompt_for_signin_ = interactive; | |
| 56 | 60 |
| 57 const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(GetExtension()); | 61 const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(GetExtension()); |
| 58 | 62 |
| 59 // Check that the necessary information is present in the manfist. | 63 // Check that the necessary information is present in the manfist. |
|
Roger Tawa OOO till Jul 10th
2013/04/02 14:41:07
manfist --> manifest?
Michael Courage
2013/04/02 17:17:49
Done.
| |
| 60 if (oauth2_info.client_id.empty()) { | 64 if (oauth2_info.client_id.empty()) { |
| 61 error_ = identity_constants::kInvalidClientId; | 65 error_ = identity_constants::kInvalidClientId; |
| 62 return false; | 66 return false; |
| 63 } | 67 } |
| 64 | 68 |
| 65 if (oauth2_info.scopes.size() == 0) { | 69 if (oauth2_info.scopes.size() == 0) { |
| 66 error_ = identity_constants::kInvalidScopes; | 70 error_ = identity_constants::kInvalidScopes; |
| 67 return false; | 71 return false; |
| 68 } | 72 } |
| 69 | 73 |
| 70 // Balanced in OnIssueAdviceSuccess|OnMintTokenSuccess|OnMintTokenFailure| | 74 // Balanced in OnIssueAdviceSuccess|OnMintTokenSuccess|OnMintTokenFailure| |
| 71 // InstallUIAbort|OnLoginUIClosed. | 75 // InstallUIAbort|SigninFailed. |
| 72 AddRef(); | 76 AddRef(); |
| 73 | 77 |
| 74 if (!HasLoginToken()) { | 78 if (!HasLoginToken()) { |
| 75 if (StartLogin()) { | 79 if (!should_prompt_for_signin_) { |
| 76 return true; | 80 error_ = identity_constants::kUserNotSignedIn; |
| 77 } else { | |
| 78 Release(); | 81 Release(); |
| 79 return false; | 82 return false; |
| 80 } | 83 } |
| 84 // Display a login prompt. If the subsequent mint fails, don't display the | |
| 85 // prompt again. | |
| 86 should_prompt_for_signin_ = false; | |
| 87 ShowLoginPopup(); | |
| 88 } else { | |
| 89 TokenService* token_service = TokenServiceFactory::GetForProfile(profile()); | |
| 90 refresh_token_ = token_service->GetOAuth2LoginRefreshToken(); | |
| 91 StartFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE); | |
| 81 } | 92 } |
| 82 | 93 |
| 83 if (StartFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE)) { | 94 return true; |
| 84 return true; | |
| 85 } else { | |
| 86 Release(); | |
| 87 return false; | |
| 88 } | |
| 89 } | 95 } |
| 90 | 96 |
| 91 void IdentityGetAuthTokenFunction::OnMintTokenSuccess( | 97 void IdentityGetAuthTokenFunction::OnMintTokenSuccess( |
| 92 const std::string& access_token) { | 98 const std::string& access_token) { |
| 93 SetResult(Value::CreateStringValue(access_token)); | 99 SetResult(Value::CreateStringValue(access_token)); |
| 94 SendResponse(true); | 100 SendResponse(true); |
| 95 Release(); // Balanced in RunImpl. | 101 Release(); // Balanced in RunImpl. |
| 96 } | 102 } |
| 97 | 103 |
| 98 void IdentityGetAuthTokenFunction::OnMintTokenFailure( | 104 void IdentityGetAuthTokenFunction::OnMintTokenFailure( |
| 99 const GoogleServiceAuthError& error) { | 105 const GoogleServiceAuthError& error) { |
| 106 switch (error.state()) { | |
| 107 case GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS: | |
| 108 case GoogleServiceAuthError::ACCOUNT_DELETED: | |
| 109 case GoogleServiceAuthError::ACCOUNT_DISABLED: | |
| 110 extensions::IdentityAPI::GetFactoryInstance()->GetForProfile( | |
| 111 profile())->ReportAuthError(error); | |
| 112 if (should_prompt_for_signin_) { | |
| 113 // Display a login prompt and try again (once). | |
| 114 should_prompt_for_signin_ = false; | |
| 115 ShowLoginPopup(); | |
| 116 return; | |
| 117 } | |
| 118 break; | |
| 119 default: | |
| 120 // Return error to caller. | |
| 121 break; | |
| 122 } | |
| 123 | |
| 100 error_ = std::string(identity_constants::kAuthFailure) + error.ToString(); | 124 error_ = std::string(identity_constants::kAuthFailure) + error.ToString(); |
| 101 SendResponse(false); | 125 SendResponse(false); |
| 102 Release(); // Balanced in RunImpl. | 126 Release(); // Balanced in RunImpl. |
| 103 } | 127 } |
| 104 | 128 |
| 105 void IdentityGetAuthTokenFunction::OnIssueAdviceSuccess( | 129 void IdentityGetAuthTokenFunction::OnIssueAdviceSuccess( |
| 106 const IssueAdviceInfo& issue_advice) { | 130 const IssueAdviceInfo& issue_advice) { |
| 131 should_prompt_for_signin_ = false; | |
| 107 // Existing grant was revoked and we used NO_FORCE, so we got info back | 132 // Existing grant was revoked and we used NO_FORCE, so we got info back |
| 108 // instead. | 133 // instead. |
| 109 if (interactive_) { | 134 if (should_prompt_for_scopes_) { |
| 110 install_ui_.reset(new ExtensionInstallPrompt(GetAssociatedWebContents())); | 135 install_ui_.reset(new ExtensionInstallPrompt(GetAssociatedWebContents())); |
| 111 ShowOAuthApprovalDialog(issue_advice); | 136 ShowOAuthApprovalDialog(issue_advice); |
| 112 } else { | 137 } else { |
| 113 error_ = identity_constants::kNoGrant; | 138 error_ = identity_constants::kNoGrant; |
| 114 SendResponse(false); | 139 SendResponse(false); |
| 115 Release(); // Balanced in RunImpl. | 140 Release(); // Balanced in RunImpl. |
| 116 } | 141 } |
| 117 } | 142 } |
| 118 | 143 |
| 119 void IdentityGetAuthTokenFunction::OnLoginUIClosed( | 144 void IdentityGetAuthTokenFunction::SigninSuccess(const std::string& token) { |
| 120 LoginUIService::LoginUI* ui) { | 145 refresh_token_ = token; |
| 121 StopObservingLoginService(); | 146 StartFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE); |
| 122 if (!StartFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE)) { | 147 } |
| 123 SendResponse(false); | 148 |
| 124 Release(); | 149 void IdentityGetAuthTokenFunction::SigninFailed() { |
| 125 } | 150 error_ = identity_constants::kUserNotSignedIn; |
|
Roger Tawa OOO till Jul 10th
2013/04/02 14:41:07
clear |refresh_token_| ?
Michael Courage
2013/04/02 17:17:49
Once we trigger the response, the whole object is
| |
| 151 SendResponse(false); | |
| 152 Release(); | |
| 126 } | 153 } |
| 127 | 154 |
| 128 void IdentityGetAuthTokenFunction::InstallUIProceed() { | 155 void IdentityGetAuthTokenFunction::InstallUIProceed() { |
| 129 DCHECK(install_ui_->record_oauth2_grant()); | 156 DCHECK(install_ui_->record_oauth2_grant()); |
| 130 // The user has accepted the scopes, so we may now force (recording a grant | 157 // The user has accepted the scopes, so we may now force (recording a grant |
| 131 // and receiving a token). | 158 // and receiving a token). |
| 132 bool success = StartFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_FORCE); | 159 StartFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_FORCE); |
| 133 DCHECK(success); | |
| 134 } | 160 } |
| 135 | 161 |
| 136 void IdentityGetAuthTokenFunction::InstallUIAbort(bool user_initiated) { | 162 void IdentityGetAuthTokenFunction::InstallUIAbort(bool user_initiated) { |
| 137 error_ = identity_constants::kUserRejected; | 163 error_ = identity_constants::kUserRejected; |
| 138 SendResponse(false); | 164 SendResponse(false); |
| 139 Release(); // Balanced in RunImpl. | 165 Release(); // Balanced in RunImpl. |
| 140 } | 166 } |
| 141 | 167 |
| 142 bool IdentityGetAuthTokenFunction::StartFlow(OAuth2MintTokenFlow::Mode mode) { | 168 void IdentityGetAuthTokenFunction::StartFlow(OAuth2MintTokenFlow::Mode mode) { |
| 143 if (!HasLoginToken()) { | 169 signin_flow_.reset(NULL); |
| 144 error_ = identity_constants::kUserNotSignedIn; | 170 mint_token_flow_.reset(CreateMintTokenFlow(mode)); |
| 145 return false; | 171 mint_token_flow_->Start(); |
| 146 } | |
| 147 | |
| 148 flow_.reset(CreateMintTokenFlow(mode)); | |
| 149 flow_->Start(); | |
| 150 return true; | |
| 151 } | |
| 152 | |
| 153 bool IdentityGetAuthTokenFunction::StartLogin() { | |
| 154 if (!interactive_) { | |
| 155 error_ = identity_constants::kUserNotSignedIn; | |
| 156 return false; | |
| 157 } | |
| 158 | |
| 159 ShowLoginPopup(); | |
| 160 return true; | |
| 161 } | |
| 162 | |
| 163 void IdentityGetAuthTokenFunction::StartObservingLoginService() { | |
| 164 LoginUIService* login_ui_service = | |
| 165 LoginUIServiceFactory::GetForProfile(profile()); | |
| 166 login_ui_service->AddObserver(this); | |
| 167 } | |
| 168 | |
| 169 void IdentityGetAuthTokenFunction::StopObservingLoginService() { | |
| 170 LoginUIService* login_ui_service = | |
| 171 LoginUIServiceFactory::GetForProfile(profile()); | |
| 172 login_ui_service->RemoveObserver(this); | |
| 173 } | 172 } |
| 174 | 173 |
| 175 void IdentityGetAuthTokenFunction::ShowLoginPopup() { | 174 void IdentityGetAuthTokenFunction::ShowLoginPopup() { |
| 176 StartObservingLoginService(); | 175 signin_flow_.reset(new IdentitySigninFlow(this, profile())); |
| 177 | 176 signin_flow_->Start(); |
| 178 LoginUIService* login_ui_service = | |
| 179 LoginUIServiceFactory::GetForProfile(profile()); | |
| 180 login_ui_service->ShowLoginPopup(); | |
| 181 } | 177 } |
| 182 | 178 |
| 183 void IdentityGetAuthTokenFunction::ShowOAuthApprovalDialog( | 179 void IdentityGetAuthTokenFunction::ShowOAuthApprovalDialog( |
| 184 const IssueAdviceInfo& issue_advice) { | 180 const IssueAdviceInfo& issue_advice) { |
| 185 install_ui_->ConfirmIssueAdvice(this, GetExtension(), issue_advice); | 181 install_ui_->ConfirmIssueAdvice(this, GetExtension(), issue_advice); |
| 186 } | 182 } |
| 187 | 183 |
| 188 OAuth2MintTokenFlow* IdentityGetAuthTokenFunction::CreateMintTokenFlow( | 184 OAuth2MintTokenFlow* IdentityGetAuthTokenFunction::CreateMintTokenFlow( |
| 189 OAuth2MintTokenFlow::Mode mode) { | 185 OAuth2MintTokenFlow::Mode mode) { |
| 190 const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(GetExtension()); | 186 const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(GetExtension()); |
| 191 TokenService* token_service = TokenServiceFactory::GetForProfile(profile()); | |
| 192 return new OAuth2MintTokenFlow( | 187 return new OAuth2MintTokenFlow( |
| 193 profile()->GetRequestContext(), | 188 profile()->GetRequestContext(), |
| 194 this, | 189 this, |
| 195 OAuth2MintTokenFlow::Parameters( | 190 OAuth2MintTokenFlow::Parameters( |
| 196 token_service->GetOAuth2LoginRefreshToken(), | 191 refresh_token_, |
| 197 GetExtension()->id(), | 192 GetExtension()->id(), |
| 198 oauth2_info.client_id, | 193 oauth2_info.client_id, |
| 199 oauth2_info.scopes, | 194 oauth2_info.scopes, |
| 200 mode)); | 195 mode)); |
| 201 } | 196 } |
| 202 | 197 |
| 203 bool IdentityGetAuthTokenFunction::HasLoginToken() const { | 198 bool IdentityGetAuthTokenFunction::HasLoginToken() const { |
| 204 TokenService* token_service = TokenServiceFactory::GetForProfile(profile()); | 199 TokenService* token_service = TokenServiceFactory::GetForProfile(profile()); |
| 205 return token_service->HasOAuthLoginToken(); | 200 return token_service->HasOAuthLoginToken(); |
| 206 } | 201 } |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 249 SendResponse(true); | 244 SendResponse(true); |
| 250 Release(); // Balanced in RunImpl. | 245 Release(); // Balanced in RunImpl. |
| 251 } | 246 } |
| 252 | 247 |
| 253 void IdentityLaunchWebAuthFlowFunction::OnAuthFlowFailure() { | 248 void IdentityLaunchWebAuthFlowFunction::OnAuthFlowFailure() { |
| 254 error_ = identity_constants::kInvalidRedirect; | 249 error_ = identity_constants::kInvalidRedirect; |
| 255 SendResponse(false); | 250 SendResponse(false); |
| 256 Release(); // Balanced in RunImpl. | 251 Release(); // Balanced in RunImpl. |
| 257 } | 252 } |
| 258 | 253 |
| 259 IdentityAPI::IdentityAPI(Profile* profile) { | 254 IdentityAPI::IdentityAPI(Profile* profile) |
| 255 : profile_(profile), | |
| 256 signin_manager_(NULL), | |
| 257 error_(GoogleServiceAuthError::NONE) { | |
| 260 (new OAuth2ManifestHandler)->Register(); | 258 (new OAuth2ManifestHandler)->Register(); |
| 261 } | 259 } |
| 262 | 260 |
| 263 IdentityAPI::~IdentityAPI() { | 261 IdentityAPI::~IdentityAPI() { |
| 264 } | 262 } |
| 265 | 263 |
| 264 void IdentityAPI::Initialize() { | |
| 265 signin_manager_ = SigninManagerFactory::GetForProfile(profile_); | |
| 266 signin_manager_->signin_global_error()->AddProvider(this); | |
| 267 | |
| 268 TokenService* token_service = TokenServiceFactory::GetForProfile(profile_); | |
| 269 registrar_.Add(this, | |
| 270 chrome::NOTIFICATION_TOKEN_AVAILABLE, | |
| 271 content::Source<TokenService>(token_service)); | |
| 272 } | |
| 273 | |
| 274 void IdentityAPI::ReportAuthError(const GoogleServiceAuthError& error) { | |
| 275 if (!signin_manager_) | |
| 276 Initialize(); | |
| 277 | |
| 278 error_ = error; | |
| 279 signin_manager_->signin_global_error()->AuthStatusChanged(); | |
| 280 } | |
| 281 | |
| 282 void IdentityAPI::Shutdown() { | |
| 283 if (signin_manager_) | |
| 284 signin_manager_->signin_global_error()->RemoveProvider(this); | |
| 285 } | |
| 286 | |
| 266 static base::LazyInstance<ProfileKeyedAPIFactory<IdentityAPI> > | 287 static base::LazyInstance<ProfileKeyedAPIFactory<IdentityAPI> > |
| 267 g_factory = LAZY_INSTANCE_INITIALIZER; | 288 g_factory = LAZY_INSTANCE_INITIALIZER; |
| 268 | 289 |
| 269 // static | 290 // static |
| 270 ProfileKeyedAPIFactory<IdentityAPI>* IdentityAPI::GetFactoryInstance() { | 291 ProfileKeyedAPIFactory<IdentityAPI>* IdentityAPI::GetFactoryInstance() { |
| 271 return &g_factory.Get(); | 292 return &g_factory.Get(); |
| 272 } | 293 } |
| 273 | 294 |
| 295 GoogleServiceAuthError IdentityAPI::GetAuthStatus() const { | |
| 296 return error_; | |
| 297 } | |
| 298 | |
| 299 void IdentityAPI::Observe(int type, | |
| 300 const content::NotificationSource& source, | |
| 301 const content::NotificationDetails& details) { | |
| 302 CHECK(type == chrome::NOTIFICATION_TOKEN_AVAILABLE); | |
| 303 TokenService::TokenAvailableDetails* token_details = | |
| 304 content::Details<TokenService::TokenAvailableDetails>(details).ptr(); | |
| 305 if (token_details->service() == | |
| 306 GaiaConstants::kGaiaOAuth2LoginRefreshToken) { | |
| 307 error_ = GoogleServiceAuthError::AuthErrorNone(); | |
| 308 signin_manager_->signin_global_error()->AuthStatusChanged(); | |
| 309 } | |
| 310 } | |
| 311 | |
| 312 template <> | |
| 313 void ProfileKeyedAPIFactory<IdentityAPI>::DeclareFactoryDependencies() { | |
| 314 DependsOn(ExtensionSystemFactory::GetInstance()); | |
| 315 DependsOn(TokenServiceFactory::GetInstance()); | |
| 316 DependsOn(SigninManagerFactory::GetInstance()); | |
| 317 } | |
| 318 | |
| 274 } // namespace extensions | 319 } // namespace extensions |
| OLD | NEW |