Mozilla/mozilla/camino/src/browser/KeychainService.mm
2005-06-29 01:25:35 +00:00

1167 lines
36 KiB
Plaintext

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is mozilla.org code.
*
* The Initial Developer of the Original Code is
* Netscape Communications Corporation.
* Portions created by the Initial Developer are Copyright (C) 1998
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Benoit Foucher <bfoucher@mac.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
#import "NSString+Utils.h"
#import "KeychainService.h"
#import "CHBrowserService.h"
#import "PreferenceManager.h"
#import <CoreServices/CoreServices.h>
#import <Carbon/Carbon.h>
#include "nsIPref.h"
#include "nsIObserverService.h"
#include "nsIObserver.h"
#include "nsCRT.h"
#include "nsString.h"
#include "nsNetUtil.h"
#include "nsReadableUtils.h"
#include "nsUnicharUtils.h"
#include "nsIWindowWatcher.h"
#include "nsIPrompt.h"
#include "nsIDOMWindowInternal.h"
#include "nsIDocument.h"
#include "nsIDOMHTMLDocument.h"
#include "nsIDOMHTMLCollection.h"
#include "nsIDOMHTMLFormElement.h"
#include "nsIDOMHTMLInputElement.h"
#include "nsIDOMHTMLSelectElement.h"
#include "nsIDOMHTMLOptionElement.h"
#include "nsIURL.h"
#include "nsIDOMWindowCollection.h"
#include "nsIContent.h"
#include "nsIWindowWatcher.h"
#include "nsIWebBrowserChrome.h"
#include "nsIEmbeddingSiteWindow.h"
#include "nsAppDirectoryServiceDefs.h"
// from CHBrowserService.h
extern NSString* const XPCOMShutDownNotificationName;
nsresult
FindUsernamePasswordFields(nsIDOMHTMLFormElement* inFormElement, nsIDOMHTMLInputElement** outUsername,
nsIDOMHTMLInputElement** outPassword, PRBool inStopWhenFound);
@implementation KeychainService
static KeychainService *sInstance = nil;
static const char* const gUseKeychainPref = "chimera.store_passwords_with_keychain";
static const char* const gAutoFillEnabledPref = "chimera.keychain_passwords_autofill";
int KeychainPrefChangedCallback(const char* pref, void* data);
//
// KeychainPrefChangedCallback
//
// Pref callback to tell us when the pref values for using the keychain
// have changed. We need to re-cache them at that time.
//
int KeychainPrefChangedCallback(const char* inPref, void* unused)
{
BOOL success = NO;
if ( strcmp(inPref, gUseKeychainPref) == 0 )
[KeychainService instance]->mIsEnabled = [[PreferenceManager sharedInstance] getBooleanPref:gUseKeychainPref withSuccess:&success];
else if ( strcmp(inPref, gAutoFillEnabledPref) == 0)
[KeychainService instance]->mIsAutoFillEnabled = [[PreferenceManager sharedInstance] getBooleanPref:gAutoFillEnabledPref withSuccess:&success];
return NS_OK;
}
+ (KeychainService*) instance
{
return sInstance ? sInstance : sInstance = [[self alloc] init];
}
- (id) init
{
if ( (self = [super init]) ) {
// Add a new form submit observer. We explicitly hold a ref in case the
// observer service uses a weakref.
nsCOMPtr<nsIObserverService> svc = do_GetService("@mozilla.org/observer-service;1");
NS_ASSERTION(svc, "Keychain can't get observer service");
mFormSubmitObserver = new KeychainFormSubmitObserver();
if ( mFormSubmitObserver && svc ) {
NS_ADDREF(mFormSubmitObserver);
svc->AddObserver(mFormSubmitObserver, NS_FORMSUBMIT_SUBJECT, PR_FALSE);
}
// register for the cocoa notification posted when XPCOM shutdown so we
// can unregister the pref callbacks we register below
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(shutdown:) name:XPCOMShutDownNotificationName
object:nil];
// cache the values of the prefs and register pref-changed callbacks. Yeah, I know
// nsIPref is obsolete, but i'm not about to create an nsIObserver just for this.
mIsEnabled = NO;
mIsAutoFillEnabled = NO;
nsCOMPtr<nsIPref> pref(do_GetService(NS_PREF_CONTRACTID));
if ( pref ) {
BOOL success = NO;
mIsEnabled = [[PreferenceManager sharedInstance] getBooleanPref:gUseKeychainPref withSuccess:&success];
mIsAutoFillEnabled = [[PreferenceManager sharedInstance] getBooleanPref:gAutoFillEnabledPref withSuccess:&success];
if ( pref ) {
pref->RegisterCallback(gUseKeychainPref, KeychainPrefChangedCallback, nsnull);
pref->RegisterCallback(gAutoFillEnabledPref, KeychainPrefChangedCallback, nsnull);
}
}
// load the keychain.nib file with our dialogs in it
BOOL success = [NSBundle loadNibNamed:@"Keychain" owner:self];
NS_ASSERTION(success, "can't load keychain prompt dialogs");
}
return self;
}
- (void) dealloc
{
// unregister for shutdown notification. It may have already happened, but just in case.
NS_IF_RELEASE(mFormSubmitObserver);
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
}
//
// shutdown:
//
// Called in response to the cocoa notification "XPCOM Shutdown" sent by the cocoa
// browser service before it terminates embedding and shuts down xpcom. Allows us
// to get rid of anything we're holding onto for the length of the app.
//
- (void) shutdown:(id)unused
{
// unregister ourselves as listeners of prefs before prefs go away
nsCOMPtr<nsIPref> pref(do_GetService(NS_PREF_CONTRACTID));
if ( pref ) {
pref->UnregisterCallback(gUseKeychainPref, KeychainPrefChangedCallback, nsnull);
pref->UnregisterCallback(gAutoFillEnabledPref, KeychainPrefChangedCallback, nsnull);
}
[sInstance release];
}
- (BOOL) isEnabled
{
return mIsEnabled;
}
- (BOOL) isAutoFillEnabled
{
return mIsAutoFillEnabled;
}
//
// getUsernameAndPassword:user:password:item
//
// looks up the username and password based on the realm. |username| and |pwd| must
// be existing, non-null NSMutableStrings and are filled in by this method. It
// also fills in the keychain's reference to this item in |outItemRef| so that
// routines following this don't have to look it up again to make changes.
//
- (BOOL) getUsernameAndPassword:(NSString*)realm port:(PRInt32)inPort user:(NSMutableString*)username password:(NSMutableString*)pwd item:(KCItemRef*)outItemRef
{
if ( !outItemRef )
return false;
if ( !realm || ![realm length] )
return false;
const int kBufferLen = 255;
OSStatus status;
char buffer[kBufferLen];
UInt32 actualSize;
if ( inPort == -1 )
inPort = kAnyPort;
if(kcfindinternetpassword([realm UTF8String], 0, 0, inPort, kKCProtocolTypeHTTP, kKCAuthTypeHTTPDigest,
kBufferLen, buffer, &actualSize, outItemRef) != noErr)
return false;
//
// Set password and username
//
buffer[actualSize] = NULL;
[pwd setString:[NSString stringWithUTF8String:buffer]];
KCAttribute attr;
attr.tag = kAccountKCItemAttr;
attr.length = kBufferLen;
attr.data = buffer;
status = KCGetAttribute( *outItemRef, &attr, &actualSize );
buffer[actualSize] = NULL;
[username setString:[NSString stringWithUTF8String:buffer]];
return true;
}
- (BOOL) findUsernameAndPassword:(NSString*)realm port:(PRInt32)inPort
{
KCItemRef ignore;
return [self getUsernameAndPassword:realm port:inPort user:[NSMutableString string] password:[NSMutableString string] item:&ignore];
}
//
// updateUsernameAndPassword:user:passowrd:item
//
// updates an existing username and password associated with the given realm. If |inItemRef| is
// a valid keychain item, it uses that (bypassing additional queries to the chain), if not
// it will look it up based on the realm.
//
- (void) updateUsernameAndPassword:(NSString*)realm port:(PRInt32)inPort user:(NSString*)username password:(NSString*)pwd item:(KCItemRef)inItemRef
{
const int kBufferLen = 255;
OSStatus status;
char buffer[kBufferLen];
UInt32 actualSize;
if ( !inItemRef ) {
if ( inPort == -1 )
inPort = kAnyPort;
if(kcfindinternetpassword([realm UTF8String], 0, 0, inPort, kKCProtocolTypeHTTP, kKCAuthTypeHTTPDigest,
kBufferLen, buffer, &actualSize, &inItemRef) != noErr)
return;
}
KCAttribute attr;
//
// Update item username and password.
//
attr.tag = kAccountKCItemAttr;
attr.data = (char*)[username UTF8String];
attr.length = strlen(NS_REINTERPRET_CAST(const char*, attr.data));
status = KCSetAttribute(inItemRef, &attr);
if(status != noErr)
NSLog(@"Couldn't update keychain item account");
status = KCSetData(inItemRef, strlen([pwd UTF8String]), [pwd UTF8String]);
if(status != noErr)
NSLog(@"Couldn't update keychain item data");
//
// Update the item now.
//
status = KCUpdateItem(inItemRef);
if(status != noErr)
NSLog(@"Couldn't update keychain item");
}
//
// storeUsernameAndPassword:port:user:password
//
// Adds a new username/password combo to the keychain based on the given realm
//
- (void) storeUsernameAndPassword:(NSString*)realm port:(PRInt32)inPort user:(NSString*)username password:(NSString*)pwd
{
if ( inPort == -1 )
inPort = kAnyPort;
kcaddinternetpassword([realm UTF8String], 0, [username UTF8String], inPort, kKCProtocolTypeHTTP, kKCAuthTypeHTTPDigest,
strlen([pwd UTF8String]), [pwd UTF8String], 0);
}
//
// removeUsernameAndPassword:item
//
// removes the username/password combo from the keychain. If |inItemRef| is a valid item, it
// uses that. If it's a null ref, it will look it up in the keychain based on the realm.
//
- (void)removeUsernameAndPassword:(NSString*)realm port:(PRInt32)inPort item:(KCItemRef)inItemRef
{
if (!inItemRef) {
if (inPort == -1)
inPort = kAnyPort;
const int kBufferLen = 255;
char buffer[kBufferLen];
UInt32 actualSize;
kcfindinternetpassword([realm UTF8String], NULL, NULL, inPort, kKCProtocolTypeHTTP, kKCAuthTypeHTTPDigest,
kBufferLen, buffer, &actualSize, &inItemRef);
}
if (inItemRef)
KCDeleteItem(inItemRef);
}
- (void)removeAllUsernamesAndPasswords
{
const int kBufferLen = 255;
char buffer[kBufferLen];
UInt32 actualSize;
KCItemRef itemRef = NULL;
while (TRUE) {
kcfindinternetpassword(NULL, NULL, NULL, kAnyPort, kKCProtocolTypeHTTP, kKCAuthTypeHTTPDigest,
kBufferLen, buffer, &actualSize, &itemRef);
if (itemRef)
KCDeleteItem(itemRef);
else
break;
itemRef = NULL;
}
}
//
// addListenerToView:
//
// Add a listener to the view to auto fill username and passwords
// fields if the values are stored in the Keychain.
//
- (void) addListenerToView:(CHBrowserView*)view
{
[view addListener:[[[KeychainBrowserListener alloc] initWithBrowser:view] autorelease]];
}
//
// hitButtonOK:
// hitButtonCancel:
// hitButtonOther:
//
// actions for the buttons of the keychain prompt dialogs.
//
enum { kOKButton = 0, kCancelButton = 1, kOtherButton = 2 };
- (IBAction)hitButtonOK:(id)sender
{
[NSApp stopModalWithCode:kOKButton];
}
- (IBAction)hitButtonCancel:(id)sender
{
[NSApp stopModalWithCode:kCancelButton];
}
- (IBAction)hitButtonOther:(id)sender
{
[NSApp stopModalWithCode:kOtherButton];
}
//
// confirmStorePassword:
//
// Puts up a dialog when the keychain doesn't yet have an entry from
// this site asking to store it, forget it this once, or mark the site
// on a deny list so we never ask again.
//
- (KeychainPromptResult)confirmStorePassword:(NSWindow*)parent
{
int result = [NSApp runModalForWindow:confirmStorePasswordPanel relativeToWindow:parent];
[confirmStorePasswordPanel close];
// the results of hitButtonXX: map to the corresponding values in the
// |KeychainPromptResult| enum so we can just cast and return
return NS_STATIC_CAST(KeychainPromptResult, result);
}
//
// confirmChangedPassword:
//
// The password stored in the keychain differs from what the user typed
// in. Ask what they want to do to resolve the issue.
//
- (BOOL)confirmChangedPassword:(NSWindow*)parent
{
int result = [NSApp runModalForWindow:confirmChangePasswordPanel relativeToWindow:parent];
[confirmChangePasswordPanel close];
return (result == kOKButton);
}
- (void) addHostToDenyList:(NSString*)host
{
[[KeychainDenyList instance] addHost:host];
}
- (BOOL) isHostInDenyList:(NSString*)host
{
return [[KeychainDenyList instance] isHostPresent:host];
}
@end
@interface KeychainDenyList (KeychainDenyListPrivate)
- (NSString*) pathToDenyListFile;
@end
@implementation KeychainDenyList
static KeychainDenyList *sDenyListInstance = nil;
+ (KeychainDenyList*) instance
{
return sDenyListInstance ? sDenyListInstance : sDenyListInstance = [[self alloc] init];
}
- (id) init
{
if ( (self = [super init]) ) {
mDenyList = [[NSUnarchiver unarchiveObjectWithFile:[self pathToDenyListFile]] retain];
if ( !mDenyList )
mDenyList = [[NSMutableArray alloc] init];
mIsDirty = NO;
// register for the cocoa notification posted when XPCOM shutdown so we
// can release our singleton and flush the file
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(shutdown:) name:XPCOMShutDownNotificationName object:nil];
}
return self;
}
- (void) dealloc
{
[self writeToDisk];
[mDenyList release];
[super dealloc];
}
//
// shutdown:
//
// Called in response to the cocoa notification "XPCOM Shutdown" sent by the cocoa
// browser service before it terminates embedding and shuts down xpcom. Allows us
// to get rid of anything we're holding onto for the length of the app.
//
- (void) shutdown:(id)unused
{
[sDenyListInstance release];
}
//
// writeToDisk
//
// flushes the deny list to the save file in the user's profile, but only
// if it has changed since we read it in.
//
- (void) writeToDisk
{
if ( mIsDirty )
{
// XXX erm, why not save it in a format that mortals can read (like a plist???)
[NSArchiver archiveRootObject:mDenyList toFile:[self pathToDenyListFile]];
}
mIsDirty = NO;
}
- (BOOL) isHostPresent:(NSString*)host
{
return [mDenyList containsObject:host];
}
- (void) addHost:(NSString*)host
{
if ( ![self isHostPresent:host] ) {
[mDenyList addObject:host];
mIsDirty = YES;
}
}
- (void) removeHost:(NSString*)host
{
if ( [self isHostPresent:host] ) {
[mDenyList removeObject:host];
mIsDirty = YES;
}
}
//
// pathToDenyListFile
//
// returns a path ('/' delimited) that cocoa can use to point to the
// deny list save file in the current user's profile
//
- (NSString*) pathToDenyListFile
{
NSMutableString* path = [[[NSMutableString alloc] init] autorelease];
nsCOMPtr<nsIFile> appProfileDir;
NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(appProfileDir));
if ( appProfileDir ) {
nsAutoString profilePath;
appProfileDir->GetPath(profilePath);
[path setString:[NSString stringWith_nsAString:profilePath]];
[path appendString:@"/Keychain Deny List"]; // |profilePath| is '/' delimited
}
return path;
}
@end
//
// Keychain prompt implementation.
//
NS_IMPL_ISUPPORTS2(KeychainPrompt,
nsIAuthPrompt,
nsIAuthPromptWrapper)
KeychainPrompt::KeychainPrompt()
{
}
KeychainPrompt::~KeychainPrompt()
{
// NSLog(@"Keychain prompt died.");
}
//
// TODO: add support for ftp username/password.
//
// Get server name and port from the realm ("hostname:port (realm)",
// see nsHttpChannel.cpp). we can't use CFURL routines or nsIURI
// routines because they require a protocol, and we don't have one.
//
// The given realm for an ftp server has the form ftp://<username>@<server>/<path>, see
// netwerk/protocol/ftp/src/nsFtpConnectionThread.cpp).
//
void
KeychainPrompt::ExtractHostAndPort(const PRUnichar* inRealm, NSString** outHost, PRInt32* outPort)
{
if ( !outHost || !outPort )
return;
*outHost = @"";
*outPort = kAnyPort;
NSString* realmStr = [NSString stringWithPRUnichars:inRealm];
// first check for an ftp url and pull out the server from the realm
if ( [realmStr rangeOfString:@"ftp://"].location != NSNotFound ) {
// cut out ftp://
realmStr = [realmStr substringFromIndex:strlen("ftp://")];
// cut out any of the path
NSRange pathDelimeter = [realmStr rangeOfString:@"/"];
if ( pathDelimeter.location != NSNotFound )
realmStr = [realmStr substringToIndex:pathDelimeter.location-1];
// now we're left with "username@server" with username being optional
NSRange usernameMarker = [realmStr rangeOfString:@"@"];
if ( usernameMarker.location != NSNotFound )
*outHost = [realmStr substringFromIndex:usernameMarker.location+1];
else
*outHost = realmStr;
}
else {
// we're an http url, strip off the "(realm)" part
NSRange firstParen = [realmStr rangeOfString:@"("];
if ( firstParen.location == NSNotFound )
firstParen.location = [realmStr length];
realmStr = [realmStr substringToIndex:firstParen.location-1];
// separate the host and the port
NSRange endOfHost = [realmStr rangeOfString:@":"];
if ( endOfHost.location == NSNotFound )
*outHost = realmStr;
else {
*outHost = [realmStr substringToIndex:endOfHost.location];
*outPort = [[realmStr substringFromIndex:endOfHost.location+1] intValue];
}
}
}
void
KeychainPrompt::PreFill(const PRUnichar *realm, PRUnichar **user, PRUnichar **pwd)
{
KeychainService* keychain = [KeychainService instance];
if(![keychain isEnabled] || ![keychain isAutoFillEnabled])
return;
NSString* host = nil;
PRInt32 port = -1;
ExtractHostAndPort(realm, &host, &port);
NSMutableString* username = nil;
NSMutableString* password = nil;
if ( user )
username = [NSMutableString stringWithCharacters:*user length:(*user ? nsCRT::strlen(*user) : 0)];
if ( pwd )
password = [NSMutableString stringWithCharacters:*pwd length:(*pwd ? nsCRT::strlen(*pwd) : 0)];
//
// Pre-fill user/password if found in the keychain.
//
KCItemRef ignore;
if([keychain getUsernameAndPassword:(NSString*)host port:port user:username password:password item:&ignore]) {
if ( user )
*user = [username createNewUnicodeBuffer];
if ( pwd )
*pwd = [password createNewUnicodeBuffer];
}
}
void
KeychainPrompt::ProcessPrompt(const PRUnichar* realm, bool checked, PRUnichar* user, PRUnichar *pwd)
{
NSString* host = nil;
PRInt32 port = -1;
ExtractHostAndPort(realm, &host, &port);
NSString* username = [NSString stringWithPRUnichars:user];
NSString* password = [NSString stringWithPRUnichars:pwd];
KeychainService* keychain = [KeychainService instance];
NSMutableString* origUsername = [NSMutableString string];
NSMutableString* origPwd = [NSMutableString string];
KCItemRef itemRef;
bool found = [keychain getUsernameAndPassword:(NSString*)host port:port user:origUsername password:origPwd item:&itemRef];
//
// Update, store or remove the user/password depending on the user
// choice and whether or not we found the username/password in the
// keychain.
//
if(checked && !found)
[keychain storeUsernameAndPassword:(NSString*)host port:port user:username password:password];
else if(checked && found && (![origUsername isEqualToString:username] || ![origPwd isEqualToString:password]))
[keychain updateUsernameAndPassword:(NSString*)host port:port user:username password:password item:itemRef];
else if(!checked && found)
[keychain removeUsernameAndPassword:(NSString*)host port:port item:itemRef];
}
//
// Implementation of nsIAuthPrompt
//
NS_IMETHODIMP
KeychainPrompt::Prompt(const PRUnichar *dialogTitle,
const PRUnichar *text,
const PRUnichar *passwordRealm,
PRUint32 savePassword,
const PRUnichar *defaultText,
PRUnichar **result,
PRBool *_retval)
{
if (defaultText)
*result = ToNewUnicode(nsDependentString(defaultText));
mPrompt->Prompt(dialogTitle, text, result, nsnull, nsnull, _retval);
return NS_OK;
}
NS_IMETHODIMP
KeychainPrompt::PromptUsernameAndPassword(const PRUnichar *dialogTitle,
const PRUnichar *text,
const PRUnichar *realm,
PRUint32 savePassword,
PRUnichar **user,
PRUnichar **pwd,
PRBool *_retval)
{
PreFill(realm, user, pwd);
PRBool checked = [[KeychainService instance] isEnabled];
PRUnichar* checkTitle = [NSLocalizedString(@"KeychainCheckTitle", @"") createNewUnicodeBuffer];
nsresult rv = mPrompt->PromptUsernameAndPassword(dialogTitle, text, user, pwd, checkTitle, &checked, _retval);
if ( checkTitle )
nsMemory::Free(checkTitle);
if (NS_FAILED(rv))
return rv;
if(*_retval)
ProcessPrompt(realm, checked, *user, *pwd);
return NS_OK;
}
NS_IMETHODIMP
KeychainPrompt::PromptPassword(const PRUnichar *dialogTitle,
const PRUnichar *text,
const PRUnichar *realm,
PRUint32 savePassword,
PRUnichar **pwd,
PRBool *_retval)
{
PreFill(realm, nsnull, pwd);
PRBool checked = [[KeychainService instance] isEnabled];
PRUnichar* checkTitle = [NSLocalizedString(@"KeychainCheckTitle", @"") createNewUnicodeBuffer];
nsresult rv = mPrompt->PromptPassword(dialogTitle, text, pwd, checkTitle, &checked, _retval);
if ( checkTitle )
nsMemory::Free(checkTitle);
if (NS_FAILED(rv))
return rv;
if(*_retval)
ProcessPrompt(realm, checked, nsnull, *pwd);
return NS_OK;
}
NS_IMETHODIMP
KeychainPrompt::SetPromptDialogs(nsIPrompt* dialogs)
{
mPrompt = dialogs;
return NS_OK;
}
//
// Keychain form submit observer implementation.
//
NS_IMPL_ISUPPORTS2(KeychainFormSubmitObserver,
nsIObserver,
nsIFormSubmitObserver)
KeychainFormSubmitObserver::KeychainFormSubmitObserver()
{
//NSLog(@"Keychain form submit observer created.");
}
KeychainFormSubmitObserver::~KeychainFormSubmitObserver()
{
//NSLog(@"Keychain form submit observer died.");
}
NS_IMETHODIMP
KeychainFormSubmitObserver::Observe(nsISupports *aSubject, const char *aTopic, const PRUnichar *someData)
{
return NS_OK;
}
NS_IMETHODIMP
KeychainFormSubmitObserver::Notify(nsIContent* node, nsIDOMWindowInternal* window, nsIURI* actionURL,
PRBool* cancelSubmit)
{
KeychainService* keychain = [KeychainService instance];
if (![keychain isEnabled])
return NS_OK;
nsCOMPtr<nsIDOMHTMLFormElement> formNode(do_QueryInterface(node));
if (!formNode)
return NS_OK;
// seek out the username and password fields. If there are two password fields, we don't
// want to do anything since it's probably not a signin form. Passing PR_FALSE in the last
// param will cause FUPF to look for multiple password fields and then bail if that is the
// case, seting |passwordElement| to nsnull.
nsCOMPtr<nsIDOMHTMLInputElement> usernameElement, passwordElement;
nsresult rv = FindUsernamePasswordFields(formNode, getter_AddRefs(usernameElement), getter_AddRefs(passwordElement),
PR_FALSE);
if (NS_SUCCEEDED(rv) && usernameElement && passwordElement) {
// extract username and password from the fields
nsAutoString uname, pword;
usernameElement->GetValue(uname);
passwordElement->GetValue(pword);
NSString* username = [NSString stringWith_nsAString:uname];
NSString* password = [NSString stringWith_nsAString:pword];
if ( ![username length] || ![password length] ) // bail if either is empty
return NS_OK;
nsCOMPtr<nsIDocument> doc = node->GetDocument();
if (!doc)
return NS_OK;
nsIURI* docURL = doc->GetDocumentURI();
if (!docURL)
return NS_OK;
nsCAutoString host;
PRInt32 port = -1;
docURL->GetHost(host);
docURL->GetPort(&port);
// is the host in the deny list? if yes, bail. otherwise check the keychain.
NSString* realm = [NSString stringWithCString:host.get()];
if ( [keychain isHostInDenyList:realm] )
return NS_OK;
//
// If there's already an entry in the keychain, check if the username
// and password match. If not, ask the user what they want to do and replace
// it as necessary. If there's no entry, ask if they want to remember it
// and then put it into the keychain
//
NSString* existingUser = [NSMutableString string];
NSString* existingPassword = [NSMutableString string];
KCItemRef itemRef;
BOOL foundExistingPassword = [keychain getUsernameAndPassword:realm port:port user:existingUser password:existingPassword item:&itemRef];
if ( foundExistingPassword ) {
if ( !([existingUser isEqualToString:username] && [existingPassword isEqualToString:password]) )
if ( CheckChangeDataYN(window) )
[keychain updateUsernameAndPassword:realm port:port user:username password:password item:itemRef];
}
else {
switch (CheckStorePasswordYN(window)) {
case kSave:
[keychain storeUsernameAndPassword:realm port:port user:username password:password];
break;
case kNeverRemember:
// tell the keychain we never want to be prompted about this host again
[keychain addHostToDenyList:realm];
break;
case kDontRemember:
// do nothing at all
break;
}
}
}
return NS_OK;
}
NSWindow*
KeychainFormSubmitObserver::GetNSWindow(nsIDOMWindowInternal* inWindow)
{
CHBrowserView* browserView = [CHBrowserView browserViewFromDOMWindow:inWindow];
return [browserView getNativeWindow];
}
KeychainPromptResult
KeychainFormSubmitObserver::CheckStorePasswordYN(nsIDOMWindowInternal* window)
{
NSWindow* nswindow = GetNSWindow(window);
return [[KeychainService instance] confirmStorePassword:nswindow];
}
BOOL
KeychainFormSubmitObserver::CheckChangeDataYN(nsIDOMWindowInternal* window)
{
NSWindow* nswindow = GetNSWindow(window);
return [[KeychainService instance] confirmChangedPassword:nswindow];
}
@implementation KeychainBrowserListener
- (id)initWithBrowser:(CHBrowserView*)aBrowser
{
if ( (self = [super init]) ) {
mBrowserView = aBrowser;
}
return self;
}
- (void) dealloc
{
// NSLog(@"Keychain browser listener died.");
[super dealloc];
}
//
// -fillDOMWindow:
//
// Given a dom window (the root of a page, frame, or iframe), fill in the username
// and password if there are appropriate fields in any frame within this "window". Once
// it's done, it recurses over all of the subframes and fills them in as well.
//
- (void)fillDOMWindow:(nsIDOMWindow*)inDOMWindow
{
KeychainService* keychain = [KeychainService instance];
if(![keychain isEnabled] || ![keychain isAutoFillEnabled])
return;
nsCOMPtr<nsIDOMDocument> domDoc;
inDOMWindow->GetDocument(getter_AddRefs(domDoc));
nsCOMPtr<nsIDocument> doc ( do_QueryInterface(domDoc) );
if (!doc) {
NS_ASSERTION(0, "no document available");
return;
}
nsCOMPtr<nsIDOMHTMLDocument> htmldoc(do_QueryInterface(doc));
if (!htmldoc)
return;
nsCOMPtr<nsIDOMHTMLCollection> forms;
nsresult rv = htmldoc->GetForms(getter_AddRefs(forms));
if (NS_FAILED(rv) || !forms)
return;
PRUint32 numForms;
forms->GetLength(&numForms);
//
// Seek out username and password element in all forms. If found in
// a form, check the keychain to see if the username password are
// stored and prefill the elements.
//
for (PRUint32 formX = 0; formX < numForms; formX++) {
nsCOMPtr<nsIDOMNode> formNode;
rv = forms->Item(formX, getter_AddRefs(formNode));
if (NS_FAILED(rv) || formNode == nsnull)
continue;
// search the current form for the text fields
nsCOMPtr<nsIDOMHTMLFormElement> formElement(do_QueryInterface(formNode));
if (!formElement)
continue;
nsCOMPtr<nsIDOMHTMLInputElement> usernameElement, passwordElement;
rv = FindUsernamePasswordFields(formElement, getter_AddRefs(usernameElement), getter_AddRefs(passwordElement),
PR_TRUE);
if (NS_SUCCEEDED(rv) && usernameElement && passwordElement) {
//
// We found the text field and password field. Check if there's
// a username/password stored in the keychain for this host and
// pre-fill the fields if that's the case.
//
nsIURI* docURL = doc->GetDocumentURI();
if (!docURL)
return;
NSMutableString* username = [NSMutableString string];
NSMutableString* password = [NSMutableString string];
nsCAutoString host;
docURL->GetHost(host);
NSString *hostStr = [NSString stringWithCString:host.get()];
PRInt32 port = -1;
docURL->GetPort(&port);
KCItemRef ignore;
if ([keychain getUsernameAndPassword:hostStr port:port user:username password:password item:&ignore]) {
nsAutoString user, pwd;
[username assignTo_nsAString:user];
[password assignTo_nsAString:pwd];
// if the server specifies a value attribute (bug 169760), only autofill
// the password if what we have in keychain matches what the server supplies,
// otherwise don't. Don't bother checking the password field for a value; i can't
// imagine the server ever prefilling a password
nsAutoString userValue;
usernameElement->GetAttribute(NS_LITERAL_STRING("value"), userValue);
if (!userValue.Length() || userValue.Equals(user)) {
rv = usernameElement->SetValue(user);
rv = passwordElement->SetValue(pwd);
}
}
// We found the sign-in form so return now. This means we don't
// support pages where there's multiple sign-in forms.
return;
}
} // for each form on page
// recursively check sub-frames and iframes
nsCOMPtr<nsIDOMWindowCollection> frames;
inDOMWindow->GetFrames(getter_AddRefs(frames));
if (frames) {
PRUint32 numFrames;
frames->GetLength(&numFrames);
for (PRUint32 i = 0; i < numFrames; i++) {
nsCOMPtr<nsIDOMWindow> frameNode;
frames->Item(i, getter_AddRefs(frameNode));
if (frameNode)
[self fillDOMWindow:frameNode];
}
}
}
- (void)onLoadingCompleted:(BOOL)succeeded;
{
if(!succeeded)
return;
// prime the pump with the top dom window.
nsCOMPtr<nsIDOMWindow> domWin = getter_AddRefs([mBrowserView getContentWindow]);
if (!domWin)
return;
// recursively fill frames and iFrames.
[self fillDOMWindow:domWin];
}
- (void)onLoadingStarted
{
}
- (void)onProgressChange:(long)currentBytes outOf:(long)maxBytes
{
}
- (void)onProgressChange64:(long long)currentBytes outOf:(long long)maxBytes
{
}
- (void)onLocationChange:(NSString*)urlSpec requestOK:(BOOL)isOK
{
}
- (void)onStatusChange:(NSString*)aMessage
{
}
- (void)onSecurityStateChange:(unsigned long)newState
{
}
- (void)onShowContextMenu:(int)flags domEvent:(nsIDOMEvent*)aEvent domNode:(nsIDOMNode*)aNode
{
}
- (void)onShowTooltip:(NSPoint)where withText:(NSString*)text
{
}
- (void)onHideTooltip
{
}
- (void)onPopupBlocked:(nsIURI*)inURIBlocked fromSite:(nsIURI*)inSite
{
}
- (void)onFoundShortcutIcon:(NSString*)inIconURI
{
}
@end
//
// FindUsernamePasswordFields
//
// Searches the form for the first username and password fields. If
// none are found, the out params will be nsnull. |inStopWhenFound|
// determines how we proceed once we find things. When true, we bail
// as soon as we find both a username and password field. If false, we
// continue searching the form for a 2nd password field (such as in a
// "change your password" form). If we find one, null out
// |outPassword| since we probably don't want to prefill this form.
//
nsresult
FindUsernamePasswordFields(nsIDOMHTMLFormElement* inFormElement, nsIDOMHTMLInputElement** outUsername,
nsIDOMHTMLInputElement** outPassword, PRBool inStopWhenFound)
{
if ( !outUsername || !outPassword )
return NS_ERROR_FAILURE;
*outUsername = *outPassword = nsnull;
PRBool autoCompleteOverride = [[PreferenceManager sharedInstance] getBooleanPref:"wallet.crypto.autocompleteoverride" withSuccess:NULL];
// pages can specify that they don't want autofill by setting a
// "autocomplete=off" attribute on the form.
nsAutoString autocomplete;
inFormElement->GetAttribute(NS_LITERAL_STRING("autocomplete"), autocomplete);
if ( autocomplete.EqualsIgnoreCase("off") && !autoCompleteOverride )
return NS_OK;
//
// Search the form the password field and the preceding text field
// We are only interested in signon forms, so we require
// only one password in the form. If there's more than one password
// it's probably a form to setup the password or to change the
// password. We don't want to handle this kind of form at this
// point.
//
nsCOMPtr<nsIDOMHTMLCollection> elements;
nsresult rv = inFormElement->GetElements(getter_AddRefs(elements));
if (NS_FAILED(rv) || !elements)
return NS_OK;
PRUint32 numElements;
elements->GetLength(&numElements);
for (PRUint32 elementX = 0; elementX < numElements; elementX++) {
nsCOMPtr<nsIDOMNode> elementNode;
rv = elements->Item(elementX, getter_AddRefs(elementNode));
if (NS_FAILED(rv) || !elementNode)
continue;
nsCOMPtr<nsIDOMHTMLInputElement> inputElement(do_QueryInterface(elementNode));
if (!inputElement)
continue;
nsAutoString type;
rv = inputElement->GetType(type);
if (NS_FAILED(rv))
continue;
bool isText = (type.IsEmpty() || type.Equals(NS_LITERAL_STRING("text"), nsCaseInsensitiveStringComparator()));
bool isPassword = type.Equals(NS_LITERAL_STRING("password"), nsCaseInsensitiveStringComparator());
inputElement->GetAttribute(NS_LITERAL_STRING("autocomplete"), autocomplete);
if ((!isText && !isPassword) || (autocomplete.EqualsIgnoreCase("off") && !autoCompleteOverride))
continue;
//
// If there's a second password in the form, it's probably not a
// signin form, probably a form to setup or change
// passwords. Stop here and ensure we don't store this password.
//
if (!inStopWhenFound && isPassword && *outPassword) {
NS_RELEASE(*outPassword);
*outPassword = nsnull;
return NS_OK;
}
// Keep taking text fields until we have a password, so that
// username is the field before password.
if(isText && !*outPassword) {
if (*outUsername) NS_RELEASE(*outUsername);
*outUsername = inputElement;
NS_ADDREF(*outUsername);
}
else if (isPassword && !*outPassword) {
*outPassword = inputElement;
NS_ADDREF(*outPassword);
}
// we've got everything we need, we're done.
if (inStopWhenFound && *outPassword && *outUsername)
return NS_OK;
} // for each item in form
return NS_OK;
}