diff options
author | vabr <vabr@chromium.org> | 2015-11-19 09:42:48 -0800 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-11-19 17:43:32 +0000 |
commit | b0a8964d59e03ab82709c85dd4f201cd7d44d3ba (patch) | |
tree | df0a7076bce54b21b1de221001fc5cbef686c15a | |
parent | c41ed1abe28b265bf0bd776876127ed999f7f772 (diff) | |
download | chromium_src-b0a8964d59e03ab82709c85dd4f201cd7d44d3ba.zip chromium_src-b0a8964d59e03ab82709c85dd4f201cd7d44d3ba.tar.gz chromium_src-b0a8964d59e03ab82709c85dd4f201cd7d44d3ba.tar.bz2 |
Move JS-related password manager code upstream
Moves
credential_manager.h
credential_manager.mm
js_credential_manager.h
js_credential_manager.mm
js_password_manager.h
js_password_manager.mm
resources/credential_manager.js
resources/password_controller.js
from the downstream to ios/chrome/browser/passwords.
This is the upstream part of the internal CL 310987013.
BUG=514241
Review URL: https://codereview.chromium.org/1456983002
Cr-Commit-Position: refs/heads/master@{#360617}
-rw-r--r-- | ios/chrome/browser/DEPS | 1 | ||||
-rw-r--r-- | ios/chrome/browser/passwords/credential_manager.h | 131 | ||||
-rw-r--r-- | ios/chrome/browser/passwords/credential_manager.mm | 360 | ||||
-rw-r--r-- | ios/chrome/browser/passwords/js_credential_manager.h | 50 | ||||
-rw-r--r-- | ios/chrome/browser/passwords/js_credential_manager.mm | 103 | ||||
-rw-r--r-- | ios/chrome/browser/passwords/js_password_manager.h | 65 | ||||
-rw-r--r-- | ios/chrome/browser/passwords/js_password_manager.mm | 107 | ||||
-rw-r--r-- | ios/chrome/browser/passwords/resources/credential_manager.js | 574 | ||||
-rw-r--r-- | ios/chrome/browser/passwords/resources/password_controller.js | 418 | ||||
-rw-r--r-- | ios/chrome/ios_chrome.gyp | 10 | ||||
-rw-r--r-- | ios/web/BUILD.gn | 2 | ||||
-rw-r--r-- | ios/web/ios_web.gyp | 12 | ||||
-rw-r--r-- | ios/web/public/web_state/js/credential_util.h (renamed from ios/web/web_state/js/credential_util.h) | 6 | ||||
-rw-r--r-- | ios/web/web_state/js/credential_util.mm | 2 | ||||
-rw-r--r-- | ios/web/web_state/js/credential_util_unittest.mm | 2 | ||||
-rw-r--r-- | ios/web/web_state/ui/crw_web_controller.mm | 2 |
16 files changed, 1832 insertions, 13 deletions
diff --git a/ios/chrome/browser/DEPS b/ios/chrome/browser/DEPS index 8f4750b..fbacbaa 100644 --- a/ios/chrome/browser/DEPS +++ b/ios/chrome/browser/DEPS @@ -34,6 +34,7 @@ include_rules = [ "+components/omnibox/browser", "+components/open_from_clipboard", "+components/password_manager/core/browser", + "+components/password_manager/core/common", "+components/password_manager/sync/browser", "+components/pref_registry", "+components/profile_metrics", diff --git a/ios/chrome/browser/passwords/credential_manager.h b/ios/chrome/browser/passwords/credential_manager.h new file mode 100644 index 0000000..9a0efce --- /dev/null +++ b/ios/chrome/browser/passwords/credential_manager.h @@ -0,0 +1,131 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef IOS_CHROME_BROWSER_PASSWORDS_CREDENTIAL_MANAGER_H_ +#define IOS_CHROME_BROWSER_PASSWORDS_CREDENTIAL_MANAGER_H_ + +#include <string> +#include <vector> + +#import "base/mac/scoped_nsobject.h" +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/prefs/pref_member.h" +#include "components/password_manager/core/browser/credential_manager_password_form_manager.h" +#include "components/password_manager/core/browser/credential_manager_pending_request_task.h" +#include "components/password_manager/core/browser/credential_manager_pending_require_user_mediation_task.h" +#include "components/password_manager/core/browser/password_manager_client.h" +#include "components/password_manager/core/browser/password_store.h" +#include "ios/web/public/web_state/web_state_observer.h" + +@class JSCredentialManager; + +// Implements the app-side of the CredentialManagement JavaScript API. +// Injects and listens to the injected JavaScript, owns and drives the user +// interface, and integrates with the password manager. This is the iOS +// equivalent of the upstream class CredentialManagerDispatcher. Note: Only +// activates on iOS 8 and later. +class CredentialManager + : public password_manager::CredentialManagerPasswordFormManagerDelegate, + public password_manager::CredentialManagerPendingRequestTaskDelegate, + public password_manager:: + CredentialManagerPendingRequireUserMediationTaskDelegate, + public web::WebStateObserver { + public: + CredentialManager(web::WebState* web_state, + password_manager::PasswordManagerClient* client, + password_manager::PasswordManagerDriver* driver, + JSCredentialManager* js_manager); + ~CredentialManager() override; + + // web::WebStateObserver: + void PageLoaded( + web::PageLoadCompletionStatus load_completion_status) override; + void CredentialsRequested(int request_id, + const GURL& source_url, + bool zero_click_only, + const std::vector<std::string>& federations, + bool is_user_initiated) override; + void SignedIn(int request_id, + const GURL& source_url, + const web::Credential& credential) override; + void SignedOut(int request_id, const GURL& source_url) override; + void WebStateDestroyed() override; + + // password_manager::CredentialManagerPendingRequestTaskDelegate: + bool IsZeroClickAllowed() const override; + GURL GetOrigin() const override; + void SendCredential( + int id, + const password_manager::CredentialInfo& credential) override; + password_manager::PasswordManagerClient* client() const override; + autofill::PasswordForm GetSynthesizedFormForOrigin() const override; + + // password_manager::CredentialManagerPendingRequireUserMediationTaskDelegate: + password_manager::PasswordStore* GetPasswordStore() override; + void DoneRequiringUserMediation() override; + + // CredentialManagerPasswordFormManagerDelegate: + void OnProvisionalSaveComplete() override; + + private: + // The errors that can cause a request to fail. + enum ErrorType { + // An existing request is outstanding. + ERROR_TYPE_PENDING_REQUEST = 0, + + // The password store isn't available. + ERROR_TYPE_PASSWORD_STORE_UNAVAILABLE, + + // The page origin is untrusted. + ERROR_TYPE_SECURITY_ERROR_UNTRUSTED_ORIGIN, + }; + + // Sends a message via |js_manager_| to resolve the JavaScript Promise + // associated with |request_id|. Invoked after a page-initiated credential + // event is acknowledged by the PasswordStore. + void ResolvePromise(int request_id); + + // Sends a message via |js_manager_| to reject the JavaScript Promise + // associated with |request_id_| with the given |error_type|. Invoked after a + // page-initiated credential event, store, or retrieval fails. + void RejectPromise(int request_id, ErrorType error_type); + + // Determines the currently loaded page's URL from the active WebState, but + // only if it is absolutely trusted. Does not hit the network, but still might + // be costly depending on the webview. Returns true if successful. + bool GetUrlWithAbsoluteTrust(GURL* page_url); + + // The request to retrieve credentials from the PasswordStore. + scoped_ptr<password_manager::CredentialManagerPendingRequestTask> + pending_request_; + + // The task to notify the password manager that the user was signed out. + scoped_ptr<password_manager::CredentialManagerPendingRequireUserMediationTask> + pending_require_user_mediation_; + + // Saves credentials to the PasswordStore. + scoped_ptr<password_manager::CredentialManagerPasswordFormManager> + form_manager_; + + // Injected JavaScript to provide the API to web pages. + base::scoped_nsobject<JSCredentialManager> js_manager_; + + // Client to access Chrome-specific password manager functionality. Weak. + password_manager::PasswordManagerClient* client_; + + // Driver to access embedder-specific password manager functionality. Weak. + password_manager::PasswordManagerDriver* driver_; + + // Whether zero-click sign-in is enabled. + BooleanPrefMember zero_click_sign_in_enabled_; + + // Weak pointer factory for asynchronously resolving requests. + base::WeakPtrFactory<CredentialManager> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(CredentialManager); +}; + +#endif // IOS_CHROME_BROWSER_PASSWORDS_CREDENTIAL_MANAGER_H_ diff --git a/ios/chrome/browser/passwords/credential_manager.mm b/ios/chrome/browser/passwords/credential_manager.mm new file mode 100644 index 0000000..405e22f --- /dev/null +++ b/ios/chrome/browser/passwords/credential_manager.mm @@ -0,0 +1,360 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "ios/chrome/browser/passwords/credential_manager.h" + +#include "base/ios/ios_util.h" +#import "base/ios/weak_nsobject.h" +#include "base/mac/bind_objc_block.h" +#include "base/memory/scoped_vector.h" +#include "base/message_loop/message_loop.h" +#include "base/strings/sys_string_conversions.h" +#include "components/password_manager/core/browser/password_store_consumer.h" +#include "components/password_manager/core/common/credential_manager_types.h" +#include "components/password_manager/core/common/password_manager_pref_names.h" +#import "ios/chrome/browser/passwords/js_credential_manager.h" +#import "ios/web/public/url_scheme_util.h" +#include "ios/web/public/web_state/credential.h" +#include "ios/web/public/web_state/url_verification_constants.h" +#include "ios/web/public/web_state/web_state.h" + +namespace { + +// Converts a password_manager::CredentialInfo to a web::Credential. +web::Credential WebCredentialFromCredentialInfo( + const password_manager::CredentialInfo& credential_info) { + web::Credential credential; + switch (credential_info.type) { + case password_manager::CredentialType::CREDENTIAL_TYPE_EMPTY: + credential.type = web::CredentialType::CREDENTIAL_TYPE_EMPTY; + break; + case password_manager::CredentialType::CREDENTIAL_TYPE_PASSWORD: + credential.type = web::CredentialType::CREDENTIAL_TYPE_PASSWORD; + break; + case password_manager::CredentialType::CREDENTIAL_TYPE_FEDERATED: + credential.type = web::CredentialType::CREDENTIAL_TYPE_FEDERATED; + break; + } + credential.id = credential_info.id; + credential.name = credential_info.name; + credential.avatar_url = credential_info.icon; + credential.password = credential_info.password; + credential.federation_url = credential_info.federation; + return credential; +} + +// Converts a web::Credential to a password_manager::CredentialInfo. +password_manager::CredentialInfo CredentialInfoFromWebCredential( + const web::Credential& credential) { + password_manager::CredentialInfo credential_info; + switch (credential.type) { + case web::CredentialType::CREDENTIAL_TYPE_EMPTY: + credential_info.type = + password_manager::CredentialType::CREDENTIAL_TYPE_EMPTY; + break; + case web::CredentialType::CREDENTIAL_TYPE_PASSWORD: + credential_info.type = + password_manager::CredentialType::CREDENTIAL_TYPE_PASSWORD; + break; + case web::CredentialType::CREDENTIAL_TYPE_FEDERATED: + credential_info.type = + password_manager::CredentialType::CREDENTIAL_TYPE_FEDERATED; + break; + } + credential_info.id = credential.id; + credential_info.name = credential.name; + credential_info.icon = credential.avatar_url; + credential_info.password = credential.password; + credential_info.federation = credential.federation_url; + return credential_info; +} + +} // namespace + +CredentialManager::CredentialManager( + web::WebState* web_state, + password_manager::PasswordManagerClient* client, + password_manager::PasswordManagerDriver* driver, + JSCredentialManager* js_manager) + : web::WebStateObserver(web_state), + pending_request_(nullptr), + form_manager_(nullptr), + js_manager_([js_manager retain]), + client_(client), + driver_(driver), + weak_factory_(this) { + zero_click_sign_in_enabled_.Init( + password_manager::prefs::kPasswordManagerAutoSignin, client_->GetPrefs()); +} + +CredentialManager::~CredentialManager() = default; + +void CredentialManager::PageLoaded( + web::PageLoadCompletionStatus load_completion_status) { + // Ensure the JavaScript is loaded when the page finishes loading. + web::URLVerificationTrustLevel trust_level = + web::URLVerificationTrustLevel::kNone; + const GURL page_url(web_state()->GetCurrentURL(&trust_level)); + if (!base::ios::IsRunningOnIOS8OrLater() || + trust_level != web::URLVerificationTrustLevel::kAbsolute || + !web::UrlHasWebScheme(page_url) || !web_state()->ContentIsHTML()) { + return; + } + [js_manager_ inject]; +} + +void CredentialManager::CredentialsRequested( + int request_id, + const GURL& source_url, + bool zero_click_only, + const std::vector<std::string>& federations, + bool is_user_initiated) { + // Invoked when the page invokes navigator.credentials.request(), this + // function will attempt to retrieve a Credential from the PasswordStore that + // meets the specified parameters and, if successful, send it back to the page + // via SendCredential. + DCHECK_GE(request_id, 0); + password_manager::PasswordStore* store = GetPasswordStore(); + + // If there's an outstanding request, or the PasswordStore isn't loaded yet, + // the request should fail outright and the JS Promise should be rejected + // with an appropriate error. + if (pending_request_ || !store) { + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&CredentialManager::RejectPromise, + weak_factory_.GetWeakPtr(), request_id, + pending_request_ ? ERROR_TYPE_PENDING_REQUEST + : ERROR_TYPE_PASSWORD_STORE_UNAVAILABLE)); + return; + } + + // If the page requested a zero-click credential -- one that can be returned + // without first asking the user -- and if zero-click isn't currently + // available, send back an empty credential. + if (zero_click_only && !IsZeroClickAllowed()) { + base::MessageLoop::current()->PostTask( + FROM_HERE, base::Bind(&CredentialManager::SendCredential, + weak_factory_.GetWeakPtr(), request_id, + password_manager::CredentialInfo())); + return; + } + + // If the page origin is untrusted, the request should be rejected. + GURL page_url; + if (!GetUrlWithAbsoluteTrust(&page_url)) { + RejectPromise(request_id, ERROR_TYPE_SECURITY_ERROR_UNTRUSTED_ORIGIN); + return; + } + + // Bundle up the arguments and forward them to the PasswordStore, which will + // asynchronously return the resulting Credential by invoking + // |SendCredential|. + std::vector<GURL> federation_urls; + for (const auto& federation : federations) + federation_urls.push_back(GURL(federation)); + std::vector<std::string> realms; + pending_request_.reset( + new password_manager::CredentialManagerPendingRequestTask( + this, request_id, zero_click_only, page_url, federation_urls, + realms)); + store->GetAutofillableLogins(pending_request_.get()); +} + +void CredentialManager::SignedIn(int request_id, + const GURL& source_url, + const web::Credential& credential) { + // Invoked when the page invokes navigator.credentials.notifySignedIn(), this + // function stores the signed-in |credential| and sends a message back to the + // page to resolve the Promise associated with |request_id|. + DCHECK(credential.type != web::CredentialType::CREDENTIAL_TYPE_EMPTY); + DCHECK_GE(request_id, 0); + + // Requests from untrusted origins should be rejected. + GURL page_url; + if (!GetUrlWithAbsoluteTrust(&page_url)) { + RejectPromise(request_id, ERROR_TYPE_SECURITY_ERROR_UNTRUSTED_ORIGIN); + return; + } + + // Notify the page that the notification was successful to avoid blocking the + // page while storing the credential. This is okay because the notification + // doesn't imply that the credential will be stored, just that it might be. + // It isn't the page's concern to know whether the storage took place or not. + ResolvePromise(request_id); + + // Do nothing if the password manager isn't active. + if (!client_->IsSavingAndFillingEnabledForCurrentPage()) + return; + + // Store the signed-in credential so that the user can save it, if desired. + // Prompting the user and saving are handled by the PasswordFormManager. + scoped_ptr<autofill::PasswordForm> form( + password_manager::CreatePasswordFormFromCredentialInfo( + CredentialInfoFromWebCredential(credential), page_url)); + form->skip_zero_click = !IsZeroClickAllowed(); + + // TODO(mkwst): This is a stub; we should be checking the PasswordStore to + // determine whether or not the credential exists, and calling UpdateLogin + // accordingly. + form_manager_.reset( + new password_manager::CredentialManagerPasswordFormManager( + client_, driver_->AsWeakPtr(), *form, this)); +} + +void CredentialManager::SignedOut(int request_id, const GURL& source_url) { + // Invoked when the page invokes navigator.credentials.notifySignedOut, this + // function notifies the PasswordStore that zero-click sign-in should be + // disabled for the current page origin. + DCHECK_GE(request_id, 0); + + // Requests from untrusted origins should be rejected. + GURL page_url; + if (!GetUrlWithAbsoluteTrust(&page_url)) { + RejectPromise(request_id, ERROR_TYPE_SECURITY_ERROR_UNTRUSTED_ORIGIN); + return; + } + + // The user signed out of the current page, so future zero-click credential + // requests for this page should fail: otherwise, the next time the user + // visits the page, if zero-click requests succeeded, the user might be auto- + // signed-in again with the credential that they just signed out. Forward this + // information to the PasswordStore via an asynchronous task. + password_manager::PasswordStore* store = GetPasswordStore(); + if (store) { + // Bundle the origins that are sent to the PasswordStore if the task hasn't + // yet resolved. This task lives across page-loads to enable this bundling. + if (pending_require_user_mediation_) { + pending_require_user_mediation_->AddOrigin(page_url); + } else { + pending_require_user_mediation_.reset( + new password_manager:: + CredentialManagerPendingRequireUserMediationTask( + this, page_url, std::vector<std::string>())); + + // This will result in a callback to + // CredentialManagerPendingSignedOutTask::OnGetPasswordStoreResults(). + store->GetAutofillableLogins(pending_require_user_mediation_.get()); + } + } + + // Acknowledge the page's signOut notification without waiting for the + // PasswordStore interaction to complete. The implementation of the sign-out + // notification isn't the page's concern. + ResolvePromise(request_id); +} + +void CredentialManager::WebStateDestroyed() { + // When the WebState is destroyed, clean up everything that depends on it. + js_manager_.reset(); +} + +bool CredentialManager::IsZeroClickAllowed() const { + // Zero-click sign-in is only allowed when the user hasn't turned it off and + // when the user isn't in incognito mode. + return *zero_click_sign_in_enabled_ && !client_->IsOffTheRecord(); +} + +GURL CredentialManager::GetOrigin() const { + web::URLVerificationTrustLevel trust_level = + web::URLVerificationTrustLevel::kNone; + const GURL page_url(web_state()->GetCurrentURL(&trust_level)); + DCHECK_EQ(trust_level, web::URLVerificationTrustLevel::kAbsolute); + return page_url; +} + +void CredentialManager::SendCredential( + int request_id, + const password_manager::CredentialInfo& credential) { + // Invoked when the asynchronous interaction with the PasswordStore completes, + // this function forwards a |credential| back to the page via |js_manager_| by + // resolving the JavaScript Promise associated with |request_id|. + base::WeakPtr<CredentialManager> weak_this = weak_factory_.GetWeakPtr(); + [js_manager_ + resolvePromiseWithRequestID:request_id + credential:WebCredentialFromCredentialInfo(credential) + completionHandler:^(BOOL) { + if (weak_this) + weak_this->pending_request_.reset(); + }]; +} + +password_manager::PasswordManagerClient* CredentialManager::client() const { + return client_; +} + +autofill::PasswordForm CredentialManager::GetSynthesizedFormForOrigin() const { + autofill::PasswordForm synthetic_form; + synthetic_form.origin = web_state()->GetLastCommittedURL().GetOrigin(); + synthetic_form.signon_realm = synthetic_form.origin.spec(); + synthetic_form.scheme = autofill::PasswordForm::SCHEME_HTML; + synthetic_form.ssl_valid = synthetic_form.origin.SchemeIsCryptographic() && + !client_->DidLastPageLoadEncounterSSLErrors(); + return synthetic_form; +} + +void CredentialManager::OnProvisionalSaveComplete() { + // Invoked after a credential sent up by the page was stored in a FormManager + // by |SignedIn|, this function asks the user if the password should be stored + // in the password manager. + DCHECK(form_manager_); + if (client_->IsSavingAndFillingEnabledForCurrentPage() && + !form_manager_->IsBlacklisted()) { + client_->PromptUserToSaveOrUpdatePassword( + form_manager_.Pass(), + password_manager::CredentialSourceType::CREDENTIAL_SOURCE_API, false); + } +} + +password_manager::PasswordStore* CredentialManager::GetPasswordStore() { + return client_ ? client_->GetPasswordStore() : nullptr; +} + +void CredentialManager::DoneRequiringUserMediation() { + // Invoked when the PasswordStore has finished processing the list of origins + // that should have zero-click sign-in disabled. + DCHECK(pending_require_user_mediation_); + pending_require_user_mediation_.reset(); +} + +void CredentialManager::ResolvePromise(int request_id) { + [js_manager_ resolvePromiseWithRequestID:request_id completionHandler:nil]; +} + +void CredentialManager::RejectPromise(int request_id, ErrorType error_type) { + NSString* type = nil; + NSString* message = nil; + switch (error_type) { + case ERROR_TYPE_PENDING_REQUEST: + type = base::SysUTF8ToNSString(kCredentialsPendingRequestErrorType); + message = base::SysUTF8ToNSString(kCredentialsPendingRequestErrorMessage); + break; + case ERROR_TYPE_PASSWORD_STORE_UNAVAILABLE: + type = base::SysUTF8ToNSString( + kCredentialsPasswordStoreUnavailableErrorType); + message = base::SysUTF8ToNSString( + kCredentialsPasswordStoreUnavailableErrorMessage); + break; + case ERROR_TYPE_SECURITY_ERROR_UNTRUSTED_ORIGIN: + type = base::SysUTF8ToNSString(kCredentialsSecurityErrorType); + message = base::SysUTF8ToNSString( + kCredentialsSecurityErrorMessageUntrustedOrigin); + break; + } + [js_manager_ rejectPromiseWithRequestID:request_id + errorType:type + message:message + completionHandler:nil]; +} + +bool CredentialManager::GetUrlWithAbsoluteTrust(GURL* page_url) { + web::URLVerificationTrustLevel trust_level = + web::URLVerificationTrustLevel::kNone; + const GURL possibly_untrusted_url(web_state()->GetCurrentURL(&trust_level)); + if (trust_level == web::URLVerificationTrustLevel::kAbsolute) { + *page_url = possibly_untrusted_url; + return true; + } + return false; +} diff --git a/ios/chrome/browser/passwords/js_credential_manager.h b/ios/chrome/browser/passwords/js_credential_manager.h new file mode 100644 index 0000000..d6d3490 --- /dev/null +++ b/ios/chrome/browser/passwords/js_credential_manager.h @@ -0,0 +1,50 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef IOS_CHROME_BROWSER_PASSWORDS_JS_CREDENTIAL_MANAGER_H_ +#define IOS_CHROME_BROWSER_PASSWORDS_JS_CREDENTIAL_MANAGER_H_ + +#include "ios/web/public/web_state/credential.h" +#import "ios/web/public/web_state/js/crw_js_injection_manager.h" + +namespace base { +class DictionaryValue; +} // namespace base + +// Constants for rejecting requests. +extern const char kCredentialsPendingRequestErrorType[]; +extern const char kCredentialsPendingRequestErrorMessage[]; +extern const char kCredentialsSecurityErrorType[]; +extern const char kCredentialsPasswordStoreUnavailableErrorType[]; +extern const char kCredentialsPasswordStoreUnavailableErrorMessage[]; +extern const char kCredentialsSecurityErrorMessageUntrustedOrigin[]; + +// Injects the JavaScript that implements the request credentials API and +// provides an app-side interface for interacting with it. +@interface JSCredentialManager : CRWJSInjectionManager + +// Resolves the JavaScript Promise associated with |requestID| with the +// specified |credential|. |completionHandler| will be invoked after the +// operation has completed with YES if successful. +- (void)resolvePromiseWithRequestID:(NSInteger)requestID + credential:(const web::Credential&)credential + completionHandler:(void (^)(BOOL))completionHandler; + +// Resolves the JavaScript Promise associated with |requestID|. +// |completionHandler| will be invoked after the operation has completed with +// YES if successful. +- (void)resolvePromiseWithRequestID:(NSInteger)requestID + completionHandler:(void (^)(BOOL))completionHandler; + +// Rejects the JavaScript Promise associated with |requestID| with an Error of +// the specified |errorType| and |message|. |completionHandler| will be invoked +// after the operation has completed with YES if successful. +- (void)rejectPromiseWithRequestID:(NSInteger)requestID + errorType:(NSString*)errorType + message:(NSString*)message + completionHandler:(void (^)(BOOL))completionHandler; + +@end + +#endif // IOS_CHROME_BROWSER_PASSWORDS_JS_CREDENTIAL_MANAGER_H_ diff --git a/ios/chrome/browser/passwords/js_credential_manager.mm b/ios/chrome/browser/passwords/js_credential_manager.mm new file mode 100644 index 0000000..abdbd41d --- /dev/null +++ b/ios/chrome/browser/passwords/js_credential_manager.mm @@ -0,0 +1,103 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "ios/chrome/browser/passwords/js_credential_manager.h" + +#include "base/json/json_writer.h" +#include "base/json/string_escape.h" +#include "base/logging.h" +#include "base/strings/sys_string_conversions.h" +#include "base/values.h" +#include "ios/web/public/web_state/js/credential_util.h" + +namespace { + +// Sanitizes |JSON| and wraps it in quotes so it can be injected safely in +// JavaScript. +NSString* JSONEscape(NSString* JSON) { + return base::SysUTF8ToNSString( + base::GetQuotedJSONString(base::SysNSStringToUTF8(JSON))); +} + +} // namespace + +const char kCredentialsPendingRequestErrorType[] = "PendingRequestError"; +const char kCredentialsPendingRequestErrorMessage[] = + "There is already an outstanding request"; +const char kCredentialsSecurityErrorType[] = "SecurityError"; +const char kCredentialsPasswordStoreUnavailableErrorType[] = + "PasswordStoreUnavailableError"; +const char kCredentialsPasswordStoreUnavailableErrorMessage[] = + "The password store is unavailable"; +const char kCredentialsSecurityErrorMessageUntrustedOrigin[] = + "The origin is untrusted"; + +@interface JSCredentialManager () + +// Evaluates the JavaScript in |script|, which should evaluate to a JavaScript +// boolean value. That value will be passed to |completionHandler|. +- (void)evaluateScript:(NSString*)script + completionHandler:(void (^)(BOOL))completionHandler; + +@end + +@implementation JSCredentialManager + +- (void)resolvePromiseWithRequestID:(NSInteger)requestID + credential:(const web::Credential&)credential + completionHandler:(void (^)(BOOL))completionHandler { + base::DictionaryValue credentialData; + web::CredentialToDictionaryValue(credential, &credentialData); + std::string credentialDataJSON; + base::JSONWriter::Write(credentialData, &credentialDataJSON); + NSString* script = [NSString + stringWithFormat:@"__gCrWeb['credentialManager'].resolve(%ld, %@)", + static_cast<long>(requestID), + base::SysUTF8ToNSString(credentialDataJSON)]; + [self evaluate:script + stringResultHandler:^(NSString* result, NSError* error) { + if (completionHandler) + completionHandler(!error && [result isEqualToString:@"true"]); + }]; +} + +- (void)resolvePromiseWithRequestID:(NSInteger)requestID + completionHandler:(void (^)(BOOL))completionHandler { + NSString* script = + [NSString stringWithFormat:@"__gCrWeb['credentialManager'].resolve(%ld)", + static_cast<long>(requestID)]; + [self evaluateScript:script completionHandler:completionHandler]; +} + +- (void)rejectPromiseWithRequestID:(NSInteger)requestID + errorType:(NSString*)errorType + message:(NSString*)message + completionHandler:(void (^)(BOOL))completionHandler { + NSString* script = [NSString + stringWithFormat:@"__gCrWeb['credentialManager'].reject(%ld, %@, %@)", + static_cast<long>(requestID), JSONEscape(errorType), + JSONEscape(message)]; + [self evaluateScript:script completionHandler:completionHandler]; +} + +- (void)evaluateScript:(NSString*)script + completionHandler:(void (^)(BOOL))completionHandler { + [self evaluate:script + stringResultHandler:^(NSString* result, NSError* error) { + if (completionHandler) + completionHandler(!error && [result isEqualToString:@"true"]); + }]; +} + +#pragma mark - Protected methods + +- (NSString*)scriptPath { + return @"credential_manager"; +} + +- (NSString*)presenceBeacon { + return @"__gCrWeb.credentialManager"; +} + +@end diff --git a/ios/chrome/browser/passwords/js_password_manager.h b/ios/chrome/browser/passwords/js_password_manager.h new file mode 100644 index 0000000..260ae2e --- /dev/null +++ b/ios/chrome/browser/passwords/js_password_manager.h @@ -0,0 +1,65 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef IOS_CHROME_BROWSER_PASSWORDS_JS_PASSWORD_MANAGER_H_ +#define IOS_CHROME_BROWSER_PASSWORDS_JS_PASSWORD_MANAGER_H_ + +#include "base/ios/block_types.h" +#import "ios/web/public/web_state/js/crw_js_injection_manager.h" + +@class CRWJSInjectionReceiver; + +// Loads the JavaScript file, password_controller.js, which contains password +// form parsing and autofill functions. It will be evaluated on a page that +// is known to have at least one password form (see hasPasswordForms in +// core.js.) It returns contents of those password forms and also +// registers functions that are later used to autofill them. +@interface JsPasswordManager : CRWJSInjectionManager + +// Finds any password forms on the web page. +// |completionHandler| is then called with the JSON string result (which can +// be a zero-length string if there was an error). |completionHandler| cannot be +// nil. +// For example the JSON string for a form with a single password field is: +// [{"action":null,"method":null,"usernameElement":"","usernameValue":""," +// passwords":[{"element":"","value":"asd"}]}] +- (void)findPasswordFormsWithCompletionHandler: + (void (^)(NSString*))completionHandler; + +// Extracts the password form with the given name from a web page. +// |completionHandler| is called with the JSON string containing the info about +// submitted password forms from a web page (it can be zero-length if there was +// an error). |completionHandler| cannot be nil. +// For example. the JSON string for a form with a single password field is: +// {"action":null,"method":null,"usernameElement":"","usernameValue":"", +// "passwords":[{"element":"","value":"asd"}]} +- (void)extractForm:(NSString*)formName + completionHandler:(void (^)(NSString*))completionHandler; + +// Fills in the password form specified by |JSONString| with the given +// |username| and |password|. Assumes JavaScript has been injected previously +// by calling |findPasswordFormsWithCompletionHandle| or +// |extractSubmittedFormWithCompletionHandler|. Calls |completionHandler| with +// YES if the filling of the password was successful, NO otherwise. +// |completionHandler| cannot be nil. +- (void)fillPasswordForm:(NSString*)JSONString + withUsername:(NSString*)username + password:(NSString*)password + completionHandler:(void (^)(BOOL))completionHandler; + +// Clears autofilled credentials in the specified form. Invokes +// |completionHandler| with YES if successful and NO otherwise. +- (void)clearAutofilledPasswordsInForm:(NSString*)formName + completionHandler:(void (^)(BOOL))completionHandler; + +// Fills all password fields in the form identified by |formName| with +// |password| and invokes |completionHandler| with true if any fields were +// filled. +- (void)fillPasswordForm:(NSString*)formName + withGeneratedPassword:(NSString*)password + completionHandler:(void (^)(BOOL))completionHandler; + +@end + +#endif // IOS_CHROME_BROWSER_PASSWORDS_JS_PASSWORD_MANAGER_H_ diff --git a/ios/chrome/browser/passwords/js_password_manager.mm b/ios/chrome/browser/passwords/js_password_manager.mm new file mode 100644 index 0000000..d28205b --- /dev/null +++ b/ios/chrome/browser/passwords/js_password_manager.mm @@ -0,0 +1,107 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "ios/chrome/browser/passwords/js_password_manager.h" + +#include "base/json/string_escape.h" +#include "base/logging.h" +#include "base/strings/sys_string_conversions.h" + +namespace { +// Sanitizes |JSONString| and wraps it in quotes so it can be injected safely in +// JavaScript. +NSString* JSONEscape(NSString* JSONString) { + return base::SysUTF8ToNSString( + base::GetQuotedJSONString(base::SysNSStringToUTF8(JSONString))); +} +} // namespace + +@interface JsPasswordManager () +// Injects a script that does two things: +// 1. Injects password controller JavaScript in the page. +// 2. Extracts the _submitted_ password form data from the DOM on the page. +// The result is returned in |completionHandler|. +// |completionHandler| cannot be nil. +- (void)evaluateExtraScript:(NSString*)script + completionHandler:(void (^)(NSString*))completionHandler; +@end + +@implementation JsPasswordManager + +- (void)findPasswordFormsWithCompletionHandler: + (void (^)(NSString*))completionHandler { + DCHECK(completionHandler); + [self evaluateExtraScript:@"__gCrWeb.findPasswordForms()" + completionHandler:completionHandler]; +} + +- (void)extractForm:(NSString*)formName + completionHandler:(void (^)(NSString*))completionHandler { + DCHECK(completionHandler); + NSString* extra = [NSString + stringWithFormat:@"__gCrWeb.getPasswordForm(%@)", JSONEscape(formName)]; + [self evaluateExtraScript:extra completionHandler:completionHandler]; +} + +- (void)fillPasswordForm:(NSString*)JSONString + withUsername:(NSString*)username + password:(NSString*)password + completionHandler:(void (^)(BOOL))completionHandler { + DCHECK(completionHandler); + NSString* script = [NSString + stringWithFormat:@"__gCrWeb.fillPasswordForm(%@, %@, %@)", JSONString, + JSONEscape(username), JSONEscape(password)]; + [self evaluate:script + stringResultHandler:^(NSString* result, NSError* error) { + completionHandler(!error && [result isEqualToString:@"true"]); + }]; +} + +- (void)clearAutofilledPasswordsInForm:(NSString*)formName + completionHandler:(void (^)(BOOL))completionHandler { + NSString* script = + [NSString stringWithFormat:@"__gCrWeb.clearAutofilledPasswords(%@)", + JSONEscape(formName)]; + [self evaluate:script + stringResultHandler:^(NSString* result, NSError* error) { + completionHandler(!error && [result isEqualToString:@"true"]); + }]; +} + +- (void)fillPasswordForm:(NSString*)formName + withGeneratedPassword:(NSString*)password + completionHandler:(void (^)(BOOL))completionHandler { + NSString* script = + [NSString stringWithFormat: + @"__gCrWeb.fillPasswordFormWithGeneratedPassword(%@, %@)", + JSONEscape(formName), JSONEscape(password)]; + [self evaluate:script + stringResultHandler:^(NSString* result, NSError* error) { + if (completionHandler) + completionHandler(!error && [result isEqualToString:@"true"]); + }]; +} + +#pragma mark - +#pragma mark ProtectedMethods + +- (NSString*)scriptPath { + return @"password_controller"; +} + +#pragma mark - +#pragma mark Private + +- (void)evaluateExtraScript:(NSString*)script + completionHandler:(void (^)(NSString*))completionHandler { + DCHECK(completionHandler); + [self injectDependenciesIfMissing]; + NSString* JS = [[self injectionContent] stringByAppendingString:script]; + [self evaluate:JS + stringResultHandler:^(NSString* result, NSError*) { + completionHandler(result); + }]; +} + +@end diff --git a/ios/chrome/browser/passwords/resources/credential_manager.js b/ios/chrome/browser/passwords/resources/credential_manager.js new file mode 100644 index 0000000..8e2e840 --- /dev/null +++ b/ios/chrome/browser/passwords/resources/credential_manager.js @@ -0,0 +1,574 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview JavaScript implementation of the credential management API + * defined at http://w3c.github.io/webappsec/specs/credentialmanagement. + * This is a minimal implementation that sends data to the app side to + * integrate with the password manager. When loaded, installs the API onto + * the window.navigator object. + */ + +// Namespace for all credential management stuff. __gCrWeb must have already +// been defined. +__gCrWeb['credentialManager'] = { + /** + * The next ID for forwarding a call from JS to the App and tracking its + * associated Promise resolvers and rejecters. + * @private {number} + */ + nextId_: 0, + + /** + * Tracks navigator.credentials Promise resolvers. + * @type {!Object<number, function(?Credential|undefined)>} + * @private + */ + resolvers_: {}, + + /** + * Tracks navigator.credentials Promise rejecters. + * @type {!Object<number, function(?Error)>} + * @private + */ + rejecters_: {} +}; + + +/** @enum {string} */ +__gCrWeb.credentialManager.CredentialType = { + CREDENTIAL: 'Credential', + PASSWORD_CREDENTIAL: 'PasswordCredential', + FEDERATED_CREDENTIAL: 'FederatedCredential', + PENDING_CREDENTIAL: 'PendingCredential' +}; + + +/** @typedef { + * type: __gCrWeb.credentialManager.CredentialType, + * id: string, + * name: (string|undefined), + * avatarURL: (string|undefined), + * federation: (string|undefined) + * } + */ +__gCrWeb.credentialManager.SerializedCredential; + + +/** + * Creates and returns a Promise whose resolver and rejecter functions are + * stored with the associated |requestId|. They can be accessed by calling + * |resolve| or |reject| on __gCrWeb['credentialManager'] with that ID. + * @param {number} requestId An identifier to track the resolver and rejecter + * associated with the returned Promise. + * @return {!Promise<?Credential|undefined>} A new Promise. + * @private + */ +__gCrWeb['credentialManager'].createPromise_ = function(requestId) { + return new Promise(function(resolve, reject) { + __gCrWeb['credentialManager'].resolvers_[requestId] = resolve; + __gCrWeb['credentialManager'].rejecters_[requestId] = reject; + }); +}; + + +/** + * Deletes the resolver and rejecter of the Promise associated with |requestId|. + * @param {number} requestId The identifier of the Promise. + * @private + */ +__gCrWeb['credentialManager'].removePromise_ = function(requestId) { + delete __gCrWeb['credentialManager'].rejecters_[requestId]; + delete __gCrWeb['credentialManager'].resolvers_[requestId]; +}; + + +/** + * Parses |credentialData| into a Credential object. + * @param {!__gCrWeb.credentialManager.SerializedCredential} credentialData A + * simple object representation of a Credential like might be obtained from + * Credential.prototype.serialize. + * @return {?Credential} A Credential object, or null if parsing was + * unsuccessful. + * @private + */ +__gCrWeb['credentialManager'].parseCredential_ = function(credentialData) { + var CredentialType = __gCrWeb.credentialManager.CredentialType; + switch (credentialData['type']) { + case CredentialType.CREDENTIAL: + return Credential.parse(credentialData); + case CredentialType.PASSWORD_CREDENTIAL: + return PasswordCredential.parse(credentialData); + case CredentialType.FEDERATED_CREDENTIAL: + return FederatedCredential.parse(credentialData); + case CredentialType.PENDING_CREDENTIAL: + return PendingCredential.parse(credentialData); + default: + return null; + } +}; + + +/** + * Resolves the Promise that was created with the given |requestId| with a + * Credential object parsed from |opt_credentialData| and removes that promise + * from the global state. Future attempts to resolve or reject the Promise will + * fail. + * @param {number} requestId The identifier of the Promise to resolve. + * @param {Object=} opt_credentialData An object describing a credential. If + * provided, this parameter will be parsed into the appropriate Credential + * type. + * @return {boolean} Indicates whether the Promise was successfully resolved. + */ +__gCrWeb['credentialManager'].resolve = function(requestId, + opt_credentialData) { + var resolver = __gCrWeb['credentialManager'].resolvers_[requestId]; + if (!resolver) { + return false; + } + if (opt_credentialData) { + var credential = null; + try { + credential = + __gCrWeb['credentialManager'].parseCredential_(opt_credentialData); + } catch (e) { + // Failed to parse |opt_credentialData|. The app side sent bad data. + return false; + } + resolver(credential); + } else { + resolver(); + } + __gCrWeb['credentialManager'].removePromise_(requestId); + return true; +}; + + +/** + * Rejects the Promise that was created with the given |requestId| and cleans up + * that Promise. + * @param {number} requestId The identifier of the Promise to resolve. + * @param {string} errorType The type of the Error to pass to the rejecter + * function. If not recognized, a standard JavaScript Error will be used. + * @param {string} message A human-readable description of the error. + * @return {boolean} Indicates whether the Promise was successfully rejected. + */ +__gCrWeb['credentialManager'].reject = function(requestId, errorType, message) { + var rejecter = __gCrWeb['credentialManager'].rejecters_[requestId]; + if (!rejecter) { + return false; + } + var error = null; + if (errorType == 'SecurityError') { + error = new SecurityError(message); + } else if (errorType == 'InvalidStateError') { + error = new InvalidStateError(message); + } else { + error = new Error(message); + } + rejecter(error); + __gCrWeb['credentialManager'].removePromise_(requestId); + return true; +}; + + +/** + * Sends a command representing |method| of navigator.credentials to the app + * side with the given |opt_options|. + * @param {string} command The name of the command being sent. + * @param {Object=} opt_options A dictionary of additional properties to forward + * to the app. + * @return {!Promise<?Credential|undefined>} A promise for the result. + * @private + */ +__gCrWeb['credentialManager'].invokeOnHost_ = function(command, opt_options) { + var requestId = __gCrWeb['credentialManager'].nextId_++; + var message = { + 'command': command, + 'requestId': requestId + }; + if (opt_options) { + Object.keys(opt_options).forEach(function(key) { + message[key] = opt_options[key]; + }); + } + __gCrWeb.message.invokeOnHost(message); + return __gCrWeb['credentialManager'].createPromise_(requestId); +}; + + + +/** + * Creates a new SecurityError to represent failures caused by violating + * security requirements of the API. + * @param {string} message A human-readable message describing this error. + * @extends {Error} + * @constructor + */ +function SecurityError(message) { + Error.call(this, message); + this.name = 'SecurityError'; + this.message = message; +} +SecurityError.prototype = Object.create(Error.prototype); +SecurityError.prototype.constructor = SecurityError; + + + +/** + * Creates a new InvalidStateError to represent failures caused by inconsistent + * internal state. + * @param {string} message A human-readable message describing this error. + * @extends {Error} + * @constructor + */ +function InvalidStateError(message) { + Error.call(this, message); + this.name = 'InvalidStateError'; + this.message = message; +} +InvalidStateError.prototype = Object.create(Error.prototype); +InvalidStateError.prototype.constructor = InvalidStateError; + + + +/** + * Creates a new Credential object. For more information, see + * https://w3c.github.io/webappsec/specs/credentialmanagement/#credential + * @param {string} id The credential’s identifier. This might be a username or + * email address, for instance. + * @param {string=} opt_name A name associated with the credential, intended as + * a human-understandable public name. + * @param {string=} opt_avatarUrl A URL pointing to an avatar image for the + * user. This URL MUST NOT be an a priori insecure URL. + * @constructor + */ +function Credential(id, opt_name, opt_avatarUrl) { + if (id === null || id === undefined) + throw new TypeError('id must be provided'); + /** + * The credential's identifier. Read-only. + * @type {string} + */ + this.id; + Object.defineProperty(this, 'id', { + configurable: false, + enumerable: true, + value: id, + writable: false + }); + /** + * A human-understandable public name associated with the credential. + * Read-only. + * @type {string} + */ + this.name; + Object.defineProperty(this, 'name', { + configurable: false, + enumerable: true, + value: opt_name || '', + writable: false + }); + /** + * A URL pointing to an avatar image for the user. Read-only. + * NOTE: This property name deviates from the Google JavaScript style guide + * in order to meet the public API specification. + * @type {string} + */ + this.avatarURL; + Object.defineProperty(this, 'avatarURL', { + configurable: false, + enumerable: true, + value: opt_avatarUrl || '', + writable: false + }); +} + + +/** + * Returns a simple object representation of this credential suitable for + * sending to the app side. + * @return {__gCrWeb.credentialManager.SerializedCredential} An object + * representing this credential. + */ +Credential.prototype.serialize = function() { + var serialized = { + 'type': this.constructor.name, + 'id': this.id, + 'name': this.name + }; + if (this.avatarURL && this.avatarURL.length > 0) + serialized['avatarURL'] = this.avatarURL; + return serialized; +}; + + +/** + * Parses |credentialData| into a Credential object. + * @param {!__gCrWeb.credentialManager.SerializedCredential} credentialData A + * simple object representation of a Credential like might be obtained from + * Credential.prototype.serialize. + * @return {Credential} A Credential object. + */ +Credential.parse = function(credentialData) { + return new Credential(credentialData['id'], + credentialData['name'], + credentialData['avatarURL']); +}; + + + +/** + * Creates a new PasswordCredential object. For more information, see + * https://w3c.github.io/webappsec/specs/credentialmanagement/#localcredential + * @param {string} id The credential’s identifier. This might be a username or + * email address, for instance. + * @param {string} password The credential's password. + * @param {string=} opt_name A name associated with the credential, intended as + * a human-understandable public name. + * @param {string=} opt_avatarUrl A URL pointing to an avatar image for the + * user. This URL MUST NOT be an a priori insecure URL. + * @constructor + * @extends {Credential} + */ +function PasswordCredential(id, password, opt_name, opt_avatarUrl) { + if (password === null || password === undefined) + throw new TypeError('password must be provided'); + Credential.call(this, id, opt_name, opt_avatarUrl); + var formData = new FormData(); + formData.append('username', id); + formData.append('password', password); + /** + * A FormData object, containing two entries: one named username, the other + * named password. Read-only. + * @type {FormData} + */ + this.formData; + Object.defineProperty(this, 'formData', { + configurable: false, + enumerable: true, + value: formData, + writable: false + }); + /** + * The credential's password. + * @type {string} + * @private + */ + this.password_; + Object.defineProperty(this, 'password_', { + configurable: false, + enumerable: false, + value: password, + writable: false + }); +} +PasswordCredential.prototype = Object.create(Credential.prototype); +PasswordCredential.prototype.constructor = PasswordCredential; + + +/** + * Returns a simple object representation of this credential suitable for + * sending to the app side. + * @return {__gCrWeb.credentialManager.SerializedCredential} An object + * representing this credential. + */ +PasswordCredential.prototype.serialize = function() { + var obj = Credential.prototype.serialize.call(this); + obj.password = this.password_; + return obj; +}; + + +/** + * Parses |credentialData| into a PasswordCredential object. + * @param {!__gCrWeb.credentialManager.SerializedCredential} credentialData A + * simple object representation of a PasswordCredential like might be + * obtained from PasswordCredential.prototype.serialize. + * @return {PasswordCredential} A PasswordCredential object. + */ +PasswordCredential.parse = function(credentialData) { + return new PasswordCredential(credentialData['id'], + credentialData['password'], + credentialData['name'], + credentialData['avatarURL']); +}; + + + +/** + * Creates a new FederatedCredential object. For more information, see + * https://w3c.github.io/webappsec/specs/credentialmanagement/#federatedcredential + * @param {string} id The credential’s identifier. This might be a username or + * email address, for instance. + * @param {string} federation The credential’s federation. For details regarding + * valid formats, see https://w3c.github.io/webappsec/specs/credentialmanagement/#identifying-federations + * @param {string=} opt_name A name associated with the credential, intended as + * a human-understandable public name. + * @param {string=} opt_avatarUrl A URL pointing to an avatar image for the + * user. This URL MUST NOT be an a priori insecure URL. + * @constructor + * @extends {Credential} + */ +function FederatedCredential(id, federation, opt_name, opt_avatarUrl) { + if (federation === null || federation === undefined) + throw new TypeError('federation must be provided'); + Credential.call(this, id, opt_name, opt_avatarUrl); + /** + * The credential’s federation. Read-only. + * @type {string} + */ + this.federation; + Object.defineProperty(this, 'federation', { + configurable: false, + enumerable: true, + value: federation, + writable: false + }); +} +FederatedCredential.prototype = Object.create(Credential.prototype); +FederatedCredential.prototype.constructor = FederatedCredential; + + +/** + * Returns a simple object representation of this credential suitable for + * sending to the app side. + * @return {__gCrWeb.credentialManager.SerializedCredential} An object + * representing this credential. + */ +FederatedCredential.prototype.serialize = function() { + var obj = Credential.prototype.serialize.call(this); + obj.federation = this.federation; + return obj; +}; + + +/** + * Parses |credentialData| into a FederatedCredential object. + * @param {!__gCrWeb.credentialManager.SerializedCredential} credentialData A + * simple object representation of a FederatedCredential like might be + * obtained from FederatedCredential.prototype.serialize. + * @return {!FederatedCredential} A FederatedCredential object. + */ +FederatedCredential.parse = function(credentialData) { + return new FederatedCredential(credentialData['id'], + credentialData['federation'], + credentialData['name'], + credentialData['avatarURL']); +}; + + + +/** + * Creates a new PendingCredential object. For more information, see + * https://w3c.github.io/webappsec/specs/credentialmanagement/#pendingcredential + * @param {string} id The credential’s identifier. This might be a username or + * email address, for instance. + * @param {string=} opt_name A name associated with the credential, intended as + * a human-understandable public name. + * @param {string=} opt_avatarUrl A URL pointing to an avatar image for the + * user. This URL MUST NOT be an a priori insecure URL. + * @constructor + * @extends {Credential} + */ +function PendingCredential(id, opt_name, opt_avatarUrl) { + Credential.call(this, id, opt_name, opt_avatarUrl); +} +PendingCredential.prototype = Object.create(Credential.prototype); +PendingCredential.prototype.constructor = PendingCredential; + + +/** + * Parses |credentialData| into a PendingCredential object. + * @param {!__gCrWeb.credentialManager.SerializedCredential} credentialData A + * simple object representation of a PendingCredential like might be + * obtained from PendingCredential.prototype.serialize. + * @return {!Credential} A PendingCredential object. + */ +PendingCredential.parse = function(credentialData) { + return new PendingCredential(credentialData['id'], + credentialData['name'], + credentialData['avatarURL']); +}; + + + +/** + * Implements the public Credential Management API. For more information, see + * http://w3c.github.io/webappsec/specs/credentialmanagement/#interfaces-credential-manager + * @constructor + */ +function CredentialsContainer() { +} + + +/** + * Requests a credential from the credential manager. + * @param {{suppressUI: boolean, federations: Array<string>}=} opt_options An + * optional dictionary of parameters for the request. If |suppressUI| is + * true, the returned promise will only be resolved with a credential if + * this is possible without user interaction; otherwise, the returned + * promise will be resolved with |undefined|. |federations| specifies a + * list of acceptable federation providers. For more information, see + * https://w3c.github.io/webappsec/specs/credentialmanagement/#interfaces-request-options + * @return {!Promise<?Credential|undefined>} A promise for retrieving the result + * of the request. + */ +CredentialsContainer.prototype.request = function(opt_options) { + var options = { + 'suppressUI': !!opt_options && !!opt_options['suppressUI'], + 'federations': (!!opt_options && opt_options['federations']) || [] + }; + return __gCrWeb['credentialManager'].invokeOnHost_( + 'navigator.credentials.request', options); +}; + + +/** + * Notifies the browser that the user has successfully signed in. + * @param {Credential=} opt_successfulCredential The credential that was used + * to sign in. + * @return {!Promise<?Credential|undefined>} A promise to wait for + * acknowledgement from the browser. + */ +CredentialsContainer.prototype.notifySignedIn = function( + opt_successfulCredential) { + var options = opt_successfulCredential && { + 'credential': opt_successfulCredential.serialize() + }; + return __gCrWeb['credentialManager'].invokeOnHost_( + 'navigator.credentials.notifySignedIn', options); +}; + + +/** + * Notifies the browser that the user failed to sign in. + * @param {Credential=} opt_failedCredential The credential that failed to + * sign in. + * @return {!Promise<?Credential|undefined>} A promise to wait for + * acknowledgement from the browser. + */ +CredentialsContainer.prototype.notifyFailedSignIn = function( + opt_failedCredential) { + var options = opt_failedCredential && { + 'credential': opt_failedCredential.serialize() + }; + return __gCrWeb['credentialManager'].invokeOnHost_( + 'navigator.credentials.notifyFailedSignIn', options); +}; + + +/** + * Notifies the browser that the user signed out. + * @return {!Promise<?Credential|undefined>} A promise to wait for + * acknowledgement from the browser. + */ +CredentialsContainer.prototype.notifySignedOut = function() { + return __gCrWeb['credentialManager'].invokeOnHost_( + 'navigator.credentials.notifySignedOut'); +}; + + +// Install the public interface. +window.navigator.credentials = new CredentialsContainer(); diff --git a/ios/chrome/browser/passwords/resources/password_controller.js b/ios/chrome/browser/passwords/resources/password_controller.js new file mode 100644 index 0000000..8131ff29 --- /dev/null +++ b/ios/chrome/browser/passwords/resources/password_controller.js @@ -0,0 +1,418 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This file adheres to closure-compiler conventions in order to enable +// compilation with ADVANCED_OPTIMIZATIONS. See http://goo.gl/FwOgy +// +// Installs password management functions on the |__gCrWeb| object. +// +// Finds all password forms in the current document and extracts +// their attributes and elements using the same logic as +// third_party/WebKit/Source/WebCore/html/HTMLFormElement.cpp +// +// Returns a JSON string representing an array of objects, +// where each object represents a password form with the discovered +// elements and their values. +// +// The search for password form fields follows the same algorithm +// as the WebKit implementation, see http://goo.gl/4hwh6 + +// Only install the password management functions once. +if (__gCrWeb && !__gCrWeb['fillPasswordForm']) { + + /** + * Finds all password forms in the window and returns form data as a JSON + * string. + * @return {string} Form data as a JSON string. + */ + __gCrWeb['findPasswordForms'] = function() { + var formDataList = []; + if (__gCrWeb.hasPasswordField()) { + __gCrWeb.getPasswordFormDataList(formDataList, window); + } + return __gCrWeb.stringify(formDataList); + }; + + /** + * Returns the password form with the given |name| as a JSON string. + * @param {string} name The name of the form to extract. + * @return {string} The password form. + */ + __gCrWeb['getPasswordForm'] = function(name) { + var el = __gCrWeb.common.getFormElementFromIdentifier(name); + if (!el) + return 'noPasswordsFound'; + var formData = __gCrWeb.getPasswordFormData(el); + if (!formData) + return 'noPasswordsFound'; + return __gCrWeb.stringify(formData); + }; + + /** + * Returns an array of forms on the page that match the structure described by + * |formData|. The form matching logic follows that in + * chrome/renderer/autofill/password_autofill_manager.h. + * @param {Object} formData Form data. + * @param {Object} doc A document containing formData. + * @param {string=} opt_normalizedAction The action URL to compare to. + * @return {Array.<Element>} Array of forms found. + */ + __gCrWeb.findMatchingPasswordForms = function(formData, doc, + opt_normalizedAction) { + var forms = doc.forms; + var fields = formData['fields']; + var matching = []; + for (var i = 0; i < forms.length; i++) { + var form = forms[i]; + var normalizedFormAction = opt_normalizedAction || + __gCrWeb.common.removeQueryAndReferenceFromURL( + __gCrWeb.common.absoluteURL(doc, form.action)); + if (formData.action != normalizedFormAction) { + continue; + } + + // We need to find all input fields matching |formData| in this form, + // otherwise it is the wrong form. + var inputs = form.getElementsByTagName('input'); + var foundAllFields = true; + for (var fieldIndex = 0; fieldIndex < fields.length; fieldIndex++) { + var name = fields[fieldIndex]['name']; + var value = fields[fieldIndex]['value']; + // The first field in |formData| is always the username field, + // the second is always the password field. + var findingUsername = fieldIndex == 0; + var findingPassword = fieldIndex == 1; + var foundField = false; + for (var k = 0; k < inputs.length; k++) { + var input = inputs[k]; + + // Ensure that the field is the right type. + if (findingPassword && input.type != 'password') { + continue; + } + if (!findingPassword && (input.type == 'password' || + !__gCrWeb.common.isTextField(input))) { + continue; + } + + // Skip read-only fields without a value since they cannot be filled. + if (input.readOnly && input.value == '') { + continue; + } + + // If more than one match is made, then we have an ambiguity (due to + // misuse of 'name' attribute) and the form is considered a mismatch. + if (input.name == name) { + if (foundField) { + foundField = false; + break; + } + foundField = true; + } + } + + if (!foundField) { + foundAllFields = false; + break; + } + } + + if (foundAllFields) { + matching.push(form); + } + } + return matching; + }; + + /** + * Clears autofilled credentials in the form with the specified name. + * @param {string} formName The name of the form to clear. + * @return {boolean} Whether the form was successfully cleared. + */ + __gCrWeb['clearAutofilledPasswords'] = function(formName) { + var el = __gCrWeb.common.getFormElementFromIdentifier(formName); + if (!el) + return false; + var formData = __gCrWeb.getPasswordFormData(el); + if (!formData) + return false; + var usernameElement = + __gCrWeb.getElementByNameWithParent(el, formData.usernameElement); + __gCrWeb.setAutofilled(usernameElement, false); + formData.passwords.forEach(function(password) { + var passwordElement = + __gCrWeb.getElementByNameWithParent(el, password.element); + if (__gCrWeb.isAutofilled(passwordElement)) { + __gCrWeb.setAutofilled(passwordElement, false); + passwordElement.value = ''; + } + }); + return true; + }; + + /** + * Finds the form described by |formData| and fills in the + * username and password values. + * + * This is a public function invoked by Chrome. There is no information + * passed to this function that the page does not have access to anyway. + * + * @param {!Object.<string, *>} formData Dictionary of parameters, + * including: + * 'action': <string> The form action URL; + * 'fields': {Array.{Object.<string, string>}} Field name/value pairs; + * @param {string} username The username to fill. + * @param {string} password The password to fill. + * @param {string=} opt_normalizedOrigin The origin URL to compare to. + * @return {boolean} Whether a form field has been filled. + */ + __gCrWeb['fillPasswordForm'] = function(formData, username, password, + opt_normalizedOrigin) { + return __gCrWeb.fillPasswordFormWithData( + formData, username, password, window, opt_normalizedOrigin); + }; + + /** + * Returns the element with the specified name that is a child of the + * specified parent element. + * @param {Element} parent The parent of the desired element. + * @param {string} name The name of the desired element. + * @return {Element} The element if found, otherwise null; + */ + __gCrWeb['getElementByNameWithParent'] = function(parent, name) { + if (parent.name === name) { + return parent; + } + for (var i = 0; i < parent.children.length; i++) { + var el = __gCrWeb.getElementByNameWithParent(parent.children[i], name); + if (el) { + return el; + } + } + return null; + }; + + /** + * Given a description of a form (origin, action and input fields), + * finds that form on the page and fills in the specified username + * and password. + * + * @param {Object} formData Form data. + * @param {string} username The username to fill. + * @param {string} password The password to fill. + * @param {Object} win A window or a frame containing formData. + * @param {string=} opt_normalizedOrigin The origin URL to compare to. + * @return {boolean} Whether a form field has been filled. + */ + __gCrWeb.fillPasswordFormWithData = + function(formData, username, password, win, opt_normalizedOrigin) { + var doc = win.document; + + // If unable to read the 'document' property from a frame in a different + // origin, do nothing. + if (!doc) { + return false; + } + + var origin = formData['origin']; + var normalizedOrigin = opt_normalizedOrigin || + __gCrWeb.common.removeQueryAndReferenceFromURL(win.location.href); + if (origin != normalizedOrigin) { + return false; + } + + var filled = false; + + __gCrWeb.findMatchingPasswordForms(formData, doc, opt_normalizedOrigin). + forEach(function(form) { + var usernameInput = + __gCrWeb.getElementByNameWithParent(form, formData.fields[0].name); + var passwordInput = + __gCrWeb.getElementByNameWithParent(form, formData.fields[1].name); + if (!usernameInput.disabled && !passwordInput.disabled) { + // If username was provided on a read-only field and it matches the + // requested username, fill the form. + if (usernameInput.readOnly && usernameInput.value) { + if (usernameInput.value == username) { + passwordInput.value = password; + __gCrWeb.setAutofilled(passwordInput, true); + filled = true; + } + } else { + usernameInput.value = username; + passwordInput.value = password; + __gCrWeb.setAutofilled(passwordInput, true); + __gCrWeb.setAutofilled(usernameInput, true); + filled = true; + } + } + }); + + // Recursively invoke for all frames/iframes. + var frames = win.frames; + for (var i = 0; i < frames.length; i++) { + if (__gCrWeb.fillPasswordFormWithData( + formData, username, password, frames[i], opt_normalizedOrigin)) { + filled = true; + } + } + + return filled; + }; + + /** + * Returns true if the supplied field |inputElement| was autofilled. + * @param {Element} inputElement The form field for which we need to + * acquire the autofilled indicator. + * @return {boolean} Whether inputElement was autofilled. + */ + __gCrWeb.isAutofilled = function(inputElement) { + return inputElement['__gCrWebAutofilled']; + }; + + /** + * Marks the supplied field as autofilled or not depending on the + * |value|. + * @param {Element} inputElement The form field for which the indicator + * needs to be set. + * @param {boolean} value The new value of the indicator. + */ + __gCrWeb.setAutofilled = function(inputElement, value) { + inputElement['__gCrWebAutofilled'] = value; + }; + + /** + * Selects text starting from |selectFrom| in the specified field. + * @param {string} formName The name of the form to select in. + * @param {string} fieldName The name of the field to select in. + * @param {number} selectFrom The starting index for selection. + * @return {boolean} Whether the operation was successful. + */ + __gCrWeb['selectText'] = function(formName, fieldName, selectFrom) { + var form = __gCrWeb.common.getFormElementFromIdentifier(formName); + var el = __gCrWeb.getElementByNameWithParent(form, fieldName); + if (!el) + return false; + el.selectionStart = selectFrom; + el.selectionEnd = el.value.length; + return true; + }; + + /** + * Fills all password fields in the form identified by |formName| + * with |password| and marks them as autofilled. + * + * @param {string} formName The name of the form to fill. + * @param {string} password The password to fill. + * @return {boolean} Whether a password field has been filled. + */ + __gCrWeb['fillPasswordFormWithGeneratedPassword'] = + function(formName, password) { + var form = __gCrWeb.common.getFormElementFromIdentifier(formName); + if (!form) + return false; + var fields = form.querySelectorAll('input[type=password]'); + for (var i = 0; i < fields.length; i++) { + var field = fields[i]; + field.value = password; + __gCrWeb.setAutofilled(field, true); + } + return fields.length > 0; + }; + + /** + * Finds all forms with passwords in the supplied window or frame and appends + * JS objects containing the form data to |formDataList|. + * @param {!Array.<Object>} formDataList A list that this function populates + * with descriptions of discovered forms. + * @param {Window} win A window (or frame) in which the function should + * look for password forms. + */ + __gCrWeb.getPasswordFormDataList = function(formDataList, win) { + var doc = win.document; + + // We may not be allowed to read the 'document' property from a frame + // that is in a different domain. + if (!doc) { + return; + } + + var forms = doc.forms; + for (var i = 0; i < forms.length; i++) { + var formData = __gCrWeb.getPasswordFormData(forms[i]); + if (formData) { + formDataList.push(formData); + } + } + + // Recursively invoke for all frames/iframes. + var frames = win.frames; + for (var i = 0; i < frames.length; i++) { + __gCrWeb.getPasswordFormDataList(formDataList, frames[i]); + } + }; + + /** + * Returns a JS object containing the data from |formElement|. + * @param {Element} formElement An HTML Form element. + * @return {Object} Object of data from formElement. + */ + __gCrWeb.getPasswordFormData = function(formElement) { + var inputs = formElement.getElementsByTagName('input'); + + var fields = []; + var passwords = []; + var firstPasswordIndex = 0; + for (var j = 0; j < inputs.length; j++) { + // TODO(dplotnikov): figure out a way to identify the activated + // submit, which is the button that the user has already hit + // before this code is called. + + var input = inputs[j]; + + fields.push({ + 'element': input.name, + 'type': input.type + }); + + if (!input.disabled && input.type == 'password') { + if (passwords.length == 0) { + firstPasswordIndex = j; + } + passwords.push({ + 'element': input.name, + 'value': input.value + }); + } + } + + if (passwords.length == 0) + return null; + + var usernameElement = ''; + var usernameValue = ''; + for (var j = firstPasswordIndex - 1; j >= 0; j--) { + var input = inputs[j]; + if (!input.disabled && __gCrWeb.common.isTextField(input)) { + usernameElement = input.name; + usernameValue = input.value; + break; + } + } + + var origin = __gCrWeb.common.removeQueryAndReferenceFromURL( + formElement.ownerDocument.location.href); + + return { + 'action': formElement.getAttribute('action'), + 'method': formElement.getAttribute('method'), + 'name': __gCrWeb.common.getFormIdentifier(formElement), + 'origin': origin, + 'fields': fields, + 'usernameElement': usernameElement, + 'usernameValue': usernameValue, + 'passwords': passwords + }; + }; +} diff --git a/ios/chrome/ios_chrome.gyp b/ios/chrome/ios_chrome.gyp index 18e7e4c..e17a037 100644 --- a/ios/chrome/ios_chrome.gyp +++ b/ios/chrome/ios_chrome.gyp @@ -356,10 +356,16 @@ 'browser/ntp_snippets/ios_chrome_ntp_snippets_service_factory.h', 'browser/open_from_clipboard/create_clipboard_recent_content.h', 'browser/open_from_clipboard/create_clipboard_recent_content.mm', + 'browser/passwords/credential_manager.h', + 'browser/passwords/credential_manager.mm', 'browser/passwords/ios_chrome_password_manager_setting_migrator_service_factory.cc', 'browser/passwords/ios_chrome_password_manager_setting_migrator_service_factory.h', 'browser/passwords/ios_chrome_password_store_factory.cc', 'browser/passwords/ios_chrome_password_store_factory.h', + 'browser/passwords/js_credential_manager.h', + 'browser/passwords/js_credential_manager.mm', + 'browser/passwords/js_password_manager.h', + 'browser/passwords/js_password_manager.mm', 'browser/passwords/password_generation_edit_view.h', 'browser/passwords/password_generation_edit_view.mm', 'browser/passwords/password_generation_offer_view.h', @@ -609,11 +615,15 @@ 'type': 'none', 'sources': [ 'browser/find_in_page/resources/find_in_page.js', + 'browser/passwords/resources/credential_manager.js', + 'browser/passwords/resources/password_controller.js', ], 'includes': [ '../../ios/web/js_compile.gypi' ], 'link_settings': { 'mac_bundle_resources': [ + '<(SHARED_INTERMEDIATE_DIR)/credential_manager.js', '<(SHARED_INTERMEDIATE_DIR)/find_in_page.js', + '<(SHARED_INTERMEDIATE_DIR)/password_controller.js', ], }, }, diff --git a/ios/web/BUILD.gn b/ios/web/BUILD.gn index 3701f47..c2ca4a0 100644 --- a/ios/web/BUILD.gn +++ b/ios/web/BUILD.gn @@ -159,6 +159,7 @@ source_set("web") { "public/web_state/crw_web_view_proxy.h", "public/web_state/crw_web_view_scroll_view_proxy.h", "public/web_state/global_web_state_observer.h", + "public/web_state/js/credential_util.h", "public/web_state/js/crw_js_injection_evaluator.h", "public/web_state/js/crw_js_injection_manager.h", "public/web_state/js/crw_js_injection_receiver.h", @@ -204,7 +205,6 @@ source_set("web") { "web_state/global_web_state_event_tracker.cc", "web_state/global_web_state_event_tracker.h", "web_state/global_web_state_observer.cc", - "web_state/js/credential_util.h", "web_state/js/credential_util.mm", "web_state/js/crw_js_early_script_manager.h", "web_state/js/crw_js_early_script_manager.mm", diff --git a/ios/web/ios_web.gyp b/ios/web/ios_web.gyp index 16e4018..814fec5 100644 --- a/ios/web/ios_web.gyp +++ b/ios/web/ios_web.gyp @@ -100,8 +100,8 @@ 'navigation/time_smoother.h', 'navigation/web_load_params.h', 'navigation/web_load_params.mm', - 'net/cert_host_pair.h', 'net/cert_host_pair.cc', + 'net/cert_host_pair.h', 'net/cert_policy.cc', 'net/cert_store_impl.cc', 'net/cert_store_impl.h', @@ -182,6 +182,7 @@ 'public/web_state/crw_web_view_proxy.h', 'public/web_state/crw_web_view_scroll_view_proxy.h', 'public/web_state/global_web_state_observer.h', + 'public/web_state/js/credential_util.h', 'public/web_state/js/crw_js_injection_evaluator.h', 'public/web_state/js/crw_js_injection_manager.h', 'public/web_state/js/crw_js_injection_receiver.h', @@ -227,7 +228,6 @@ 'web_state/global_web_state_event_tracker.cc', 'web_state/global_web_state_event_tracker.h', 'web_state/global_web_state_observer.cc', - 'web_state/js/credential_util.h', 'web_state/js/credential_util.mm', 'web_state/js/crw_js_early_script_manager.h', 'web_state/js/crw_js_early_script_manager.mm', @@ -289,6 +289,8 @@ 'web_state/web_view_internal_creation_util.mm', 'web_state/wk_web_view_security_util.h', 'web_state/wk_web_view_security_util.mm', + 'web_thread_impl.cc', + 'web_thread_impl.h', 'web_view_counter_impl.h', 'web_view_counter_impl.mm', 'web_view_creation_util.mm', @@ -313,8 +315,6 @@ 'webui/web_ui_ios_data_source_impl.h', 'webui/web_ui_ios_impl.h', 'webui/web_ui_ios_impl.mm', - 'web_thread_impl.cc', - 'web_thread_impl.h', ], 'link_settings': { # TODO(crbug.com/541549): change to regular linking once support for @@ -475,10 +475,10 @@ 'public/test/web_test_util.h', 'test/crw_fake_web_controller_observer.h', 'test/crw_fake_web_controller_observer.mm', - 'test/web_int_test.h', - 'test/web_int_test.mm', 'test/test_web_thread.cc', 'test/test_web_thread_bundle.cc', + 'test/web_int_test.h', + 'test/web_int_test.mm', 'test/web_test.h', 'test/web_test.mm', 'test/web_test_suite.cc', diff --git a/ios/web/web_state/js/credential_util.h b/ios/web/public/web_state/js/credential_util.h index 39230de..f18b274 100644 --- a/ios/web/web_state/js/credential_util.h +++ b/ios/web/public/web_state/js/credential_util.h @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef IOS_WEB_WEB_STATE_JS_CREDENTIAL_UTIL_H_ -#define IOS_WEB_WEB_STATE_JS_CREDENTIAL_UTIL_H_ +#ifndef IOS_WEB_PUBLIC_WEB_STATE_JS_CREDENTIAL_UTIL_H_ +#define IOS_WEB_PUBLIC_WEB_STATE_JS_CREDENTIAL_UTIL_H_ namespace base { class DictionaryValue; @@ -44,4 +44,4 @@ void CredentialToDictionaryValue(const Credential& credential, } // namespace web -#endif // IOS_WEB_WEB_STATE_JS_CREDENTIAL_UTIL_H_ +#endif // IOS_WEB_PUBLIC_WEB_STATE_JS_CREDENTIAL_UTIL_H_ diff --git a/ios/web/web_state/js/credential_util.mm b/ios/web/web_state/js/credential_util.mm index d0a9b4a..9017e42 100644 --- a/ios/web/web_state/js/credential_util.mm +++ b/ios/web/web_state/js/credential_util.mm @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "ios/web/web_state/js/credential_util.h" +#include "ios/web/public/web_state/js/credential_util.h" #include "base/logging.h" #include "base/strings/string16.h" diff --git a/ios/web/web_state/js/credential_util_unittest.mm b/ios/web/web_state/js/credential_util_unittest.mm index dc42abd..7982ea1 100644 --- a/ios/web/web_state/js/credential_util_unittest.mm +++ b/ios/web/web_state/js/credential_util_unittest.mm @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "ios/web/web_state/js/credential_util.h" +#include "ios/web/public/web_state/js/credential_util.h" #include "base/memory/scoped_ptr.h" #include "base/strings/utf_string_conversions.h" diff --git a/ios/web/web_state/ui/crw_web_controller.mm b/ios/web/web_state/ui/crw_web_controller.mm index 2fa81f2..28f95f4 100644 --- a/ios/web/web_state/ui/crw_web_controller.mm +++ b/ios/web/web_state/ui/crw_web_controller.mm @@ -54,6 +54,7 @@ #include "ios/web/public/web_state/credential.h" #import "ios/web/public/web_state/crw_web_controller_observer.h" #import "ios/web/public/web_state/crw_web_view_scroll_view_proxy.h" +#import "ios/web/public/web_state/js/credential_util.h" #import "ios/web/public/web_state/js/crw_js_injection_manager.h" #import "ios/web/public/web_state/js/crw_js_injection_receiver.h" #import "ios/web/public/web_state/ui/crw_content_view.h" @@ -66,7 +67,6 @@ #import "ios/web/web_state/crw_web_view_proxy_impl.h" #import "ios/web/web_state/error_translation_util.h" #include "ios/web/web_state/frame_info.h" -#import "ios/web/web_state/js/credential_util.h" #import "ios/web/web_state/js/crw_js_early_script_manager.h" #import "ios/web/web_state/js/crw_js_plugin_placeholder_manager.h" #import "ios/web/web_state/js/crw_js_window_id_manager.h" |