From cfc67706f8299f43e0fa8aec1dbae128fcfff6cf Mon Sep 17 00:00:00 2001 From: "alqahira%ardisson.org" Date: Mon, 21 Jan 2008 20:32:12 +0000 Subject: [PATCH] Camino only: Bug 178607 - Keychain cannot save multiple passwords for the same host. Patch by Bryan Atwood , r=smorgan, sr=mento. git-svn-id: svn://10.0.0.236/trunk@243675 18797224-902f-48f8-a5cc-f745e15eee43 --- .../camino/src/formfill/AutoCompleteUtils.h | 83 ++ .../camino/src/formfill/AutoCompleteUtils.mm | 87 ++ .../camino/src/formfill/FormFillController.h | 100 +++ .../camino/src/formfill/FormFillController.mm | 748 ++++++++++++++++++ mozilla/camino/src/formfill/FormFillPopup.h | 83 ++ mozilla/camino/src/formfill/FormFillPopup.mm | 230 ++++++ .../formfill/KeychainAutoCompleteSession.h | 85 ++ .../formfill/KeychainAutoCompleteSession.mm | 369 +++++++++ 8 files changed, 1785 insertions(+) create mode 100644 mozilla/camino/src/formfill/AutoCompleteUtils.h create mode 100644 mozilla/camino/src/formfill/AutoCompleteUtils.mm create mode 100644 mozilla/camino/src/formfill/FormFillController.h create mode 100644 mozilla/camino/src/formfill/FormFillController.mm create mode 100644 mozilla/camino/src/formfill/FormFillPopup.h create mode 100644 mozilla/camino/src/formfill/FormFillPopup.mm create mode 100644 mozilla/camino/src/formfill/KeychainAutoCompleteSession.h create mode 100644 mozilla/camino/src/formfill/KeychainAutoCompleteSession.mm diff --git a/mozilla/camino/src/formfill/AutoCompleteUtils.h b/mozilla/camino/src/formfill/AutoCompleteUtils.h new file mode 100644 index 00000000000..0bc6a1fc3c6 --- /dev/null +++ b/mozilla/camino/src/formfill/AutoCompleteUtils.h @@ -0,0 +1,83 @@ +/* -*- 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 Camino code. + * + * The Initial Developer of the Original Code is + * Bryan Atwood + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Bryan Atwood + * + * 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 + +// AutoCompleteResults +// +// Container object for generic auto complete. +// Holds the search string, array of matched objects and the default item. +// +@interface AutoCompleteResults : NSObject +{ + NSString* mSearchString; // strong + NSArray* mMatches; // strong + int mDefaultIndex; +} +- (NSString*)searchString; +- (void)setSearchString:(NSString*)string; + +- (NSArray*)matches; +- (void)setMatches:(NSArray*)matches; + +- (int)defaultIndex; +- (void)setDefaultIndex:(int)defaultIndex; + +@end + +// AutoCompleteListener +// +// This defines the protocol methods for the object that listens for auto complete +// results. |onAutoComplete| is called by the object that searches the data and +// the results are returned to the originating caller as AutoCompleteResults. +// +@protocol AutoCompleteListener +- (void)autoCompleteFoundResults:(AutoCompleteResults*)results; +@end + +// AutoCompleteSession +// +// An AutoCompleteSession object listens for search requests and searches a set of data +// |startAutoCompleteWithSearch| initiates the process. Previous results are passed in +// as well as the listener object for when the search is complete. +// +@protocol AutoCompleteSession +- (void)startAutoCompleteWithSearch:(NSString*)searchString + previousResults:(AutoCompleteResults*)previousSearchResults + listener:(id)listener; +@end diff --git a/mozilla/camino/src/formfill/AutoCompleteUtils.mm b/mozilla/camino/src/formfill/AutoCompleteUtils.mm new file mode 100644 index 00000000000..0e72fa11aca --- /dev/null +++ b/mozilla/camino/src/formfill/AutoCompleteUtils.mm @@ -0,0 +1,87 @@ +/* -*- 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 Camino code. + * + * The Initial Developer of the Original Code is + * Bryan Atwood + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Bryan Atwood + * + * 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 "AutoCompleteUtils.h" + +@implementation AutoCompleteResults + +- (void)dealloc +{ + [mSearchString release]; + [mMatches release]; + + [super dealloc]; +} + +- (NSString*)searchString +{ + return mSearchString; +} + +- (void)setSearchString:(NSString*)string +{ + [mSearchString release]; + + // The caller might change the string, so keep a copy. + mSearchString = [string copy]; +} + +- (NSArray*)matches +{ + return mMatches; +} + +- (void)setMatches:(NSArray*)matches +{ + if (mMatches != matches) { + [mMatches release]; + mMatches = [matches retain]; + } +} + +- (int)defaultIndex +{ + return mDefaultIndex; +} + +- (void)setDefaultIndex:(int)index +{ + mDefaultIndex = index; +} + +@end diff --git a/mozilla/camino/src/formfill/FormFillController.h b/mozilla/camino/src/formfill/FormFillController.h new file mode 100644 index 00000000000..a482e92bddd --- /dev/null +++ b/mozilla/camino/src/formfill/FormFillController.h @@ -0,0 +1,100 @@ +/* -*- 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 Camino code. + * + * The Initial Developer of the Original Code is + * Bryan Atwood + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Bryan Atwood + * + * 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 +#import "AutoCompleteUtils.h" + +#include "nsIDOMEventListener.h" + +extern const int kFormFillMaxRows; + +@class KeychainAutoCompleteSession; +@class CHBrowserView; +@class FormFillPopup; +@class FormFillController; + +class nsIDOMHTMLInputElement; + +// FormFillListener +// +// The FormFillListener object listens for DOM events and the corresponding +// methods in the FormFillController are called for handling. +// +class FormFillListener : public nsIDOMEventListener +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIDOMEVENTLISTENER + + FormFillListener(FormFillController* aController); + +protected: + FormFillController* mController; // weak +}; + +// FormFillController +// +// Manages the FormFillPopup windows that contain search results +// as well as sending search requests to the KeychainAutoCompleteSession +// and listening for search results. This can be extended to send +// search requests to a form history session as well. +@interface FormFillController : NSObject +{ + KeychainAutoCompleteSession* mKeychainSession; // strong + AutoCompleteResults* mResults; // strong + FormFillListener* mListener; // strong + FormFillPopup* mPopupWindow; // strong + + CHBrowserView* mBrowserView; // weak + nsIDOMHTMLInputElement* mFocusedInputElement; // weak + + // mCompleteResult determines if the current search should complete the default + // result when ready. This prevents backspace/delete from autocompleting. + BOOL mCompleteResult; + + // mUsernameFillEnabled determines whether we send searches to the Keychain + // Service. Form fill history value can be added here as well. + BOOL mUsernameFillEnabled; +} + +- (void)attachToBrowser:(CHBrowserView*)browser; + +// Callback function for when a row in the focused popup window is clicked. +- (void)popupSelected; + +@end diff --git a/mozilla/camino/src/formfill/FormFillController.mm b/mozilla/camino/src/formfill/FormFillController.mm new file mode 100644 index 00000000000..c74e5d1f228 --- /dev/null +++ b/mozilla/camino/src/formfill/FormFillController.mm @@ -0,0 +1,748 @@ +/* -*- 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 Camino code. + * + * The Initial Developer of the Original Code is + * Bryan Atwood + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Bryan Atwood + * + * 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 "FormFillController.h" +#import "CHBrowserView.h" +#import "FormFillPopup.h" +#import "KeychainAutoCompleteSession.h" +#import "PreferenceManager.h" + +#import "NSString+Gecko.h" +#import "NSArray+Utils.h" + +#include "GeckoUtils.h" +#include "nsString.h" +#include "nsPIDOMWindow.h" +#include "nsIDOMWindow.h" +#include "nsIDOMDocumentEvent.h" +#include "nsIPrivateDOMEvent.h" +#include "nsIDOMEventTarget.h" +#include "nsIDOMEvent.h" +#include "nsIDOMHTMLFormElement.h" +#include "nsIDOMHTMLInputElement.h" +#include "nsIDOMNSHTMLInputElement.h" +#include "nsIDOMKeyEvent.h" + +const int kFormFillMaxRows = 10; + +@interface FormFillController(Private) + +// Listener and autocomplete initialization and cleanup +- (void)browserResized:(NSNotification*)notification; +- (void)addResizeObserver:(NSWindow*)browserWindow; +- (void)removeResizeObserver:(NSWindow*)browserWindow; +- (void)addWindowListeners:(nsIDOMWindow*)aWindow; +- (void)removeWindowListeners:(nsIDOMWindow*)aWindow; +- (void)startControllingInputElement:(nsIDOMHTMLInputElement*)aInputElement; +- (void)stopControllingInputElement; + +// Popup window management +- (BOOL)isPopupOpen; +- (void)openPopup; +- (void)closePopup; +- (void)shiftRowSelectionBy:(int)aRows; + +// Autocomplete methods +- (void)startSearch:(NSString*)searchString; +- (void)dataReady; +- (void)autoCompleteFieldText; +- (void)filledAutoCompleteFieldText; + +// Event handlers +- (void)focus:(nsIDOMEvent*)aEvent; +- (void)blur:(nsIDOMEvent*)aEvent; +- (void)unload:(nsIDOMEvent*)aEvent; +- (void)submit:(nsIDOMEvent*)aEvent; +- (void)input:(nsIDOMEvent*)aEvent; +- (void)keyPress:(nsIDOMEvent*)aEvent; +- (BOOL)handleKeyNavigation:(int)aKey; + +// Utility methods +- (BOOL)IsCaretAtEndOfLine; + +@end + +NS_IMPL_ISUPPORTS1(FormFillListener, nsIDOMEventListener) + +FormFillListener::FormFillListener(FormFillController* aController) +: mController(aController) +{ +} + +NS_IMETHODIMP FormFillListener::HandleEvent(nsIDOMEvent* aEvent) +{ + nsAutoString type; + aEvent->GetType(type); + + if (type.Equals(NS_LITERAL_STRING("focus"))) + [mController focus:aEvent]; + else if (type.Equals(NS_LITERAL_STRING("blur"))) + [mController blur:aEvent]; + else if (type.Equals(NS_LITERAL_STRING("unload"))) + [mController unload:aEvent]; + else if (type.Equals(NS_LITERAL_STRING("submit"))) + [mController submit:aEvent]; + else if (type.Equals(NS_LITERAL_STRING("input"))) + [mController input:aEvent]; + else if (type.Equals(NS_LITERAL_STRING("keypress"))) + [mController keyPress:aEvent]; + return NS_OK; +} + +@implementation FormFillController + +- (id)init +{ + if ((self = [super init])) { + // mListener captures DOM and auto complete events. + mListener = new FormFillListener(self); + NS_ADDREF(mListener); + + // Initialize the password fill session. + // History form fill session can also be added when someone writes it. + mKeychainSession = [[KeychainAutoCompleteSession alloc] init]; + } + + return self; +} + +- (void)dealloc +{ + // Remove ourselves as a focus listener from cached view. + nsCOMPtr domWindow = [mBrowserView contentWindow]; + if (domWindow) + [self removeWindowListeners:domWindow]; + + [self removeResizeObserver:[mBrowserView nativeWindow]]; + + [mResults release]; + [mKeychainSession release]; + [mPopupWindow release]; + NS_IF_RELEASE(mListener); + + [super dealloc]; +} + +- (void)attachToBrowser:(CHBrowserView*)browser +{ + if (!browser) + return; + + mBrowserView = browser; + + // Listen for focus events on the domWindow of the browser view. + nsCOMPtr domWindow = [mBrowserView contentWindow]; + if (domWindow) + [self addWindowListeners:domWindow]; +} + +- (void)browserResized:(NSNotification*)notification +{ + [self closePopup]; +} + +- (void)addResizeObserver:(NSWindow*)browserWindow +{ + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(browserResized:) + name:NSWindowDidResizeNotification + object:browserWindow]; +} + +- (void)removeResizeObserver:(NSWindow*)browserWindow +{ + [[NSNotificationCenter defaultCenter] removeObserver:self + name:NSWindowDidResizeNotification + object:browserWindow]; +} + +- (void)addWindowListeners:(nsIDOMWindow*)aWindow +{ + nsCOMPtr privateDOMWindow = do_QueryInterface(aWindow); + if (!privateDOMWindow) + return; + + nsPIDOMEventTarget* chromeEventHandler = privateDOMWindow->GetChromeEventHandler(); + + nsCOMPtr target = do_QueryInterface(chromeEventHandler); + if (!target) + return; + + target->AddEventListener(NS_LITERAL_STRING("focus"), + static_cast(mListener), + PR_TRUE); + + target->AddEventListener(NS_LITERAL_STRING("blur"), + static_cast(mListener), + PR_TRUE); + + target->AddEventListener(NS_LITERAL_STRING("unload"), + static_cast(mListener), + PR_TRUE); + + target->AddEventListener(NS_LITERAL_STRING("submit"), + static_cast(mListener), + PR_TRUE); + + target->AddEventListener(NS_LITERAL_STRING("input"), + static_cast(mListener), + PR_TRUE); + + target->AddEventListener(NS_LITERAL_STRING("keypress"), + static_cast(mListener), + PR_TRUE); +} + +- (void)removeWindowListeners:(nsIDOMWindow*)aWindow +{ + [self stopControllingInputElement]; + + nsCOMPtr privateDOMWindow = do_QueryInterface(aWindow); + if (!privateDOMWindow) + return; + + nsPIDOMEventTarget* chromeEventHandler = privateDOMWindow->GetChromeEventHandler(); + + nsCOMPtr target = do_QueryInterface(chromeEventHandler); + if (!target) + return; + + target->RemoveEventListener(NS_LITERAL_STRING("focus"), + static_cast(mListener), + PR_TRUE); + + target->RemoveEventListener(NS_LITERAL_STRING("blur"), + static_cast(mListener), + PR_TRUE); + + target->RemoveEventListener(NS_LITERAL_STRING("unload"), + static_cast(mListener), + PR_TRUE); + + target->RemoveEventListener(NS_LITERAL_STRING("submit"), + static_cast(mListener), + PR_TRUE); + + target->RemoveEventListener(NS_LITERAL_STRING("input"), + static_cast(mListener), + PR_TRUE); + + target->RemoveEventListener(NS_LITERAL_STRING("keypress"), + static_cast(mListener), + PR_TRUE); +} + +- (void)startControllingInputElement:(nsIDOMHTMLInputElement*)aInputElement +{ + // Make sure we're not still attached to an input element. + [self stopControllingInputElement]; + + // Set the autocomplete session for this input. Currently, only password autocomplete + // but other sessions like form fill history can be added here. + mUsernameFillEnabled = [mKeychainSession attachToInput:aInputElement]; + + // If this is not a valid auto-complete input, we can return now. + if (!mUsernameFillEnabled) + return; + + // Cache the input element. + mFocusedInputElement = aInputElement; + + // Create the popup window if it doesn't exist. + if (!mPopupWindow) { + mPopupWindow = [[FormFillPopup alloc] init]; + [mPopupWindow attachToController:self]; + } +} + +- (void)stopControllingInputElement +{ + if (mPopupWindow) { + [self closePopup]; + [mPopupWindow setItems:nil]; + } + + mFocusedInputElement = nsnull; + + // Stop sending search requests to auto-form fill. + mUsernameFillEnabled = NO; + + [mResults release]; + mResults = nil; +} + +-(void)autoCompleteFoundResults:(AutoCompleteResults*)results +{ + [mResults release]; + mResults = nil; + + if ([[results matches] count] > 0) { + mResults = [results retain]; + [self dataReady]; + } + else { + [mPopupWindow setItems:nil]; + [self closePopup]; + } +} + +- (BOOL)isPopupOpen +{ + return mPopupWindow ? [mPopupWindow isPopupOpen] : NO; +} + +- (void)openPopup +{ + // Only open popup if it's not already open. + if ([self isPopupOpen]) + return; + + // Make sure input field is visible before showing popup. + GeckoUtils::ScrollElementIntoView(mFocusedInputElement); + + nsIntRect inputIntRect; + if (!(GeckoUtils::GetFrameInScreenCoordinates(mFocusedInputElement, &inputIntRect))) + return; + + NSRect inputElementFrame = NSMakeRect(inputIntRect.x, inputIntRect.y, inputIntRect.width, inputIntRect.height); + + NSScreen* mainScreen = [[NSScreen screens] firstObject]; // NSArray category method + if (!mainScreen) + return; + + NSPoint origin = inputElementFrame.origin; + float width = NSWidth(inputElementFrame); + + // y-flip and subtract the control height to convert to cocoa coords + origin.y = NSMaxY([mainScreen frame]) - inputElementFrame.origin.y - inputElementFrame.size.height; + + // To account for the text box border, shift rectangle position to the right by 2 pixels + // and reduce the width by 3 pixels. + // TODO: check shift here, still not aligned sometimes. + origin.x += 2.0; + width -= 3.0; + + [mPopupWindow openPopup:[mBrowserView nativeWindow] withOrigin:origin width:width]; + + // Listen for resize events to close the popup. + [self addResizeObserver:[mBrowserView nativeWindow]]; +} + +- (void)closePopup +{ + if (mPopupWindow) { + // Deselecting the row prevents a flash when popup is opened and default is selected. + [mPopupWindow selectRow:-1]; + [mPopupWindow closePopup]; + [self removeResizeObserver:[mBrowserView nativeWindow]]; + } +} + +- (void)shiftRowSelectionBy:(int)aRows +{ + int row = [mPopupWindow selectedRow] + aRows; + + // pin result at top row + if (row < 0) + row = 0; + + // pin result at bottom row + int numRows = [mPopupWindow rowCount]; + if (row >= numRows) + row = numRows - 1; + + [mPopupWindow selectRow:row]; +} + +// +// popupSelected +// +// Called when a new item in the popup window is selected, either by mouse click +// or keyboard movement. The form field is set to the value of the selected item +// and an autocomplete event is sent for any listeners +- (void)popupSelected +{ + if (![self isPopupOpen] || !mResults) + return; + + int row = [mPopupWindow selectedRow]; + if (row < 0) + return; + + nsAutoString value; + [[mPopupWindow resultForRow:row] assignTo_nsAString:value]; + mFocusedInputElement->SetValue(value); + + // Auto-fill the input so that untyped letters are selected. + nsCOMPtr nsInput = do_QueryInterface(mFocusedInputElement); + if (nsInput) { + int searchLength = [[mResults searchString] length]; + nsInput->SetSelectionStart((PRInt32)searchLength); + } + + // Send an autocomplete DOM event any time auto fill is done. + [self filledAutoCompleteFieldText]; +} + +- (void)startSearch:(NSString*)searchString; +{ + // Check if password autocomplete is enabled. + // Form history autocomplete can be added here. + if (mUsernameFillEnabled) { + [mKeychainSession startAutoCompleteWithSearch:searchString + previousResults:mResults + listener:self]; + } +} + +- (void)dataReady +{ + [mPopupWindow setItems:[mResults matches]]; + + // Open the popup if more than one result is returned. + // Also require mCompleteResult since it prevents a backspace from opening popup + // even though we want to search on a backspace to get the larger result set. + if ([mPopupWindow rowCount] > 1 && mCompleteResult) + [self openPopup]; + else + [self closePopup]; + + // Prevents backspace from auto-completing the result we just erased. + if (mCompleteResult) + [self autoCompleteFieldText]; +} + +// +// autoCompleteFieldText +// +// Called when FormFillController should fill the text field with the default +// autocomplete result. +- (void)autoCompleteFieldText +{ + if (!mResults) + return; + + NSArray* matches = [mResults matches]; + + // Select the default if it's available. + // Otherwise, select the first username in the list. + int defaultIndex = [mResults defaultIndex]; + + nsAutoString value; + [[matches objectAtIndex:defaultIndex] assignTo_nsAString:value]; + mFocusedInputElement->SetValue(value); + + // Auto-fill the input such that untyped letters are selected. + nsCOMPtr nsInput = do_QueryInterface(mFocusedInputElement); + if (nsInput) { + int searchLength = [[mResults searchString] length]; + nsInput->SetSelectionStart((PRInt32)searchLength); + } + + // Select the default entry in the popup. + if ([self isPopupOpen]) + [mPopupWindow selectRow:defaultIndex]; + + // Send a DOM event any time auto fill is done. + [self filledAutoCompleteFieldText]; +} + +// +// filledAutoCompleteFieldText +// +// Called whenever the form element is filled. It sends a DOM event +// "DOMAutoComplete" that can be listened for, e.g., to fill a password +// when a username element is completed. +// +- (void)filledAutoCompleteFieldText +{ + if (!mFocusedInputElement) + return; + + nsCOMPtr domDoc; + mFocusedInputElement->GetOwnerDocument(getter_AddRefs(domDoc)); + + nsCOMPtr doc = do_QueryInterface(domDoc); + if (!doc) + return; + + nsCOMPtr event; + doc->CreateEvent(NS_LITERAL_STRING("Events"), getter_AddRefs(event)); + nsCOMPtr privateEvent = do_QueryInterface(event); + if (!privateEvent) + return; + + event->InitEvent(NS_LITERAL_STRING("DOMAutoComplete"), PR_TRUE, PR_TRUE); + + privateEvent->SetTrusted(PR_TRUE); + + nsCOMPtr target = do_QueryInterface(mFocusedInputElement); + + PRBool defaultActionEnabled; + target->DispatchEvent(event, &defaultActionEnabled); +} + +// +// focus +// +// When a new element is selected, check whether we allow autocomplete. +// If autocomplete is allowed, start controlling the input to that element. +// +- (void)focus:(nsIDOMEvent*)aEvent +{ + nsCOMPtr target; + aEvent->GetTarget(getter_AddRefs(target)); + + nsCOMPtr inputElement = do_QueryInterface(target); + if (!inputElement) + return; + + nsAutoString type; + inputElement->GetType(type); + if (!type.LowerCaseEqualsLiteral("text")) + return; + + PRBool isReadOnly = PR_FALSE; + if (NS_FAILED(inputElement->GetReadOnly(&isReadOnly)) || isReadOnly) + return; + + PRBool autoCompleteOverride = [[PreferenceManager sharedInstance] getBooleanPref:"wallet.crypto.autocompleteoverride" withSuccess:NULL]; + + if (!autoCompleteOverride) { + nsAutoString autocomplete; + inputElement->GetAttribute(NS_LITERAL_STRING("autocomplete"), autocomplete); + + if (autocomplete.EqualsIgnoreCase("off")) + return; + + nsCOMPtr form; + inputElement->GetForm(getter_AddRefs(form)); + if (form) { + form->GetAttribute(NS_LITERAL_STRING("autocomplete"), autocomplete); + if (autocomplete.EqualsIgnoreCase("off")) + return; + } + } + + [self startControllingInputElement:inputElement]; +} + +// +// blur +// +// Anytime focus moves from an element, stop autocompleting it until next +// input element is focused. +// +- (void)blur:(nsIDOMEvent*)aEvent +{ + if (mFocusedInputElement) + [self stopControllingInputElement]; +} + +// +// unload +// +// Stop autocompleting on a page if it is unloaded. +// +- (void)unload:(nsIDOMEvent*)aEvent +{ + if (mFocusedInputElement) { + nsCOMPtr target; + aEvent->GetTarget(getter_AddRefs(target)); + + nsCOMPtr eventDoc = do_QueryInterface(target); + nsCOMPtr inputDoc; + mFocusedInputElement->GetOwnerDocument(getter_AddRefs(inputDoc)); + + if (eventDoc == inputDoc) + [self stopControllingInputElement]; + } +} + +// +// submit +// +// If a form is submitted, stop autocompleting. +// +- (void)submit:(nsIDOMEvent*)aEvent +{ + if (mFocusedInputElement) + [self stopControllingInputElement]; +} + +// +// input +// +// If an HTML input box is being controlled, do a search when input occurs. +// +- (void)input:(nsIDOMEvent*)aEvent +{ + nsCOMPtr target; + aEvent->GetTarget(getter_AddRefs(target)); + + nsCOMPtr input = do_QueryInterface(target); + + if (input && mFocusedInputElement == input) { + nsAutoString value; + mFocusedInputElement->GetValue(value); + [self startSearch:[NSString stringWith_nsAString:value]]; + } +} + +// +// keyPress +// +// This is triggered when a key is pressed but before an 'input' is triggered. +// It also handles non-input keys like arrow keys. +// +- (void)keyPress:(nsIDOMEvent*)aEvent +{ + if (!mFocusedInputElement) + return; + + nsCOMPtr keyEvent = do_QueryInterface(aEvent); + if (!keyEvent) + return; + + // By default, allow keystroke to continue to the next listener. + BOOL cancel = NO; + + // By default, autocomplete on keystrokes that later trigger 'input' events. + mCompleteResult = YES; + + PRUint32 k; + keyEvent->GetKeyCode(&k); + switch (k) { + case nsIDOMKeyEvent::DOM_VK_UP: + case nsIDOMKeyEvent::DOM_VK_DOWN: + case nsIDOMKeyEvent::DOM_VK_PAGE_UP: + case nsIDOMKeyEvent::DOM_VK_PAGE_DOWN: + cancel = [self handleKeyNavigation:k]; + break; + case nsIDOMKeyEvent::DOM_VK_ESCAPE: + case nsIDOMKeyEvent::DOM_VK_RETURN: + cancel = [self isPopupOpen]; + [self closePopup]; + break; + case nsIDOMKeyEvent::DOM_VK_BACK_SPACE: + case nsIDOMKeyEvent::DOM_VK_DELETE: + // Don't allow autocomplete of 'input' event if it's due to a back space or + // delete. + mCompleteResult = NO; + break; + } + + if (cancel) { + aEvent->StopPropagation(); + aEvent->PreventDefault(); + } +} + +// +// handleKeyNavigation +// +// Handles key events that are for navigation of the popup window. +// Should return YES if the event should be cancelled and not propagated. +// +- (BOOL)handleKeyNavigation:(int)aKey +{ + switch (aKey) { + case nsIDOMKeyEvent::DOM_VK_UP: + if ([self isPopupOpen]) { + [self shiftRowSelectionBy:-1]; + [self popupSelected]; + return YES; + } + break; + case nsIDOMKeyEvent::DOM_VK_DOWN: + if ([self isPopupOpen]) { + [self shiftRowSelectionBy:1]; + [self popupSelected]; + return YES; + } + else if ([self IsCaretAtEndOfLine]) { + nsAutoString value; + mFocusedInputElement->GetValue(value); + [self startSearch:[NSString stringWith_nsAString:value]]; + return YES; + } + break; + case nsIDOMKeyEvent::DOM_VK_PAGE_UP: + if ([self isPopupOpen]) { + [self shiftRowSelectionBy:-kFormFillMaxRows]; + [self popupSelected]; + return YES; + } + break; + case nsIDOMKeyEvent::DOM_VK_PAGE_DOWN: + if ([self isPopupOpen]) { + [self shiftRowSelectionBy:kFormFillMaxRows]; + [self popupSelected]; + return YES; + } + else { + nsAutoString value; + mFocusedInputElement->GetValue(value); + [self startSearch:[NSString stringWith_nsAString:value]]; + return YES; + } + break; + } + + return NO; +} + +- (BOOL)IsCaretAtEndOfLine +{ + if (!mFocusedInputElement) + return NO; + + nsCOMPtr nsInput = do_QueryInterface(mFocusedInputElement); + if (!nsInput) + return NO; + + PRInt32 selectStart; + nsInput->GetSelectionStart(&selectStart); + + PRInt32 textLength; + nsInput->GetTextLength(&textLength); + + return (selectStart == textLength); +} + +@end diff --git a/mozilla/camino/src/formfill/FormFillPopup.h b/mozilla/camino/src/formfill/FormFillPopup.h new file mode 100644 index 00000000000..c44cb1fb830 --- /dev/null +++ b/mozilla/camino/src/formfill/FormFillPopup.h @@ -0,0 +1,83 @@ +/* -*- 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 Camino code. + * + * The Initial Developer of the Original Code is + * Bryan Atwood + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Bryan Atwood + * + * 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 + +@class FormFillController; + +// +// FormFillPopupWindow +// +// The popup window needs to look like a "key" (activated) window even thought it's +// a child window. This subclass overrides |isKeyWindow| to return YES so that it is +// able to be a key window (and have activated scrollbars, etc) but not steal focus. +// +@interface FormFillPopupWindow : NSPanel +@end + +// FormFillPopup +// +// Manages the display of the popup window and receives data from the FormFillController. +// +@interface FormFillPopup : NSObject +{ + FormFillPopupWindow* mPopupWin; // strong + NSArray* mItems; // strong + + NSTableView* mTableView; // weak + FormFillController* mController; // weak +} + +- (void)attachToController:(FormFillController*)controller; + +// openPopup expects an origin point in Cocoa coordinates and a width that +// should be equal to the text box. +- (void)openPopup:(NSWindow*)browserWindow withOrigin:(NSPoint)origin width:(float)width; +- (void)resizePopup; +- (void)closePopup; +- (BOOL)isPopupOpen; + +- (int)visibleRows; +- (int)rowCount; +- (void)selectRow:(int)index; +- (int)selectedRow; + +- (void)setItems:(NSArray*)items; +- (NSString*)resultForRow:(int)index; + +@end diff --git a/mozilla/camino/src/formfill/FormFillPopup.mm b/mozilla/camino/src/formfill/FormFillPopup.mm new file mode 100644 index 00000000000..cc1b1b2a9ef --- /dev/null +++ b/mozilla/camino/src/formfill/FormFillPopup.mm @@ -0,0 +1,230 @@ +/* -*- 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 Camino code. + * + * The Initial Developer of the Original Code is + * Bryan Atwood + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Bryan Atwood + * + * 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 "FormFillPopup.h" +#import "FormFillController.h" + +@implementation FormFillPopupWindow + +- (BOOL)isKeyWindow +{ + return YES; +} + +@end + +@interface FormFillPopup(Private) + +- (void)onRowClicked:(NSNotification *)aNote; + +@end + +@implementation FormFillPopup + +- (id)init +{ + if ((self = [super init])) { + // Construct and configure the popup window. + mPopupWin = [[FormFillPopupWindow alloc] initWithContentRect:NSMakeRect(0,0,0,0) + styleMask:NSNonactivatingPanelMask + backing:NSBackingStoreBuffered + defer:NO]; + + [mPopupWin setReleasedWhenClosed:NO]; + [mPopupWin setHasShadow:YES]; + [mPopupWin setAlphaValue:0.9]; + + // Construct and configure the view. + mTableView = [[[NSTableView alloc] initWithFrame:NSMakeRect(0,0,0,0)] autorelease]; + [mTableView setIntercellSpacing:NSMakeSize(1, 2)]; + [mTableView setTarget:self]; + [mTableView setAction:@selector(onRowClicked:)]; + + // Create the text column (only one). + NSTableColumn* column = [[[NSTableColumn alloc] initWithIdentifier:@"usernames"] autorelease]; + [mTableView addTableColumn:column]; + + // Hide the table header. + [mTableView setHeaderView:nil]; + + [mTableView setDataSource:self]; + + NSScrollView *scrollView = [[[NSScrollView alloc] initWithFrame:NSMakeRect(0,0,0,0)] autorelease]; + [scrollView setHasVerticalScroller:YES]; + [scrollView setAutohidesScrollers:YES]; + [[scrollView verticalScroller] setControlSize:NSSmallControlSize]; + + [scrollView setDocumentView:mTableView]; + + [mPopupWin setContentView:scrollView]; + } + + return self; +} + +- (void)dealloc +{ + [mPopupWin release]; + [mItems release]; + + [super dealloc]; +} + +- (void)attachToController:(FormFillController*)controller +{ + mController = controller; +} + +- (BOOL)isPopupOpen +{ + return [mPopupWin isVisible]; +} + +- (void)openPopup:(NSWindow*)browserWindow withOrigin:(NSPoint)origin width:(float)width +{ + // Set the size of the popup window. + NSRect tableViewFrame = [mTableView frame]; + tableViewFrame.size.width = width; + [mTableView setFrame:tableViewFrame]; + + // Size the panel correctly. + tableViewFrame.size.height = (int)([mTableView rowHeight] + [mTableView intercellSpacing].height) * [self visibleRows]; + [mPopupWin setContentSize:tableViewFrame.size]; + [mPopupWin setFrameTopLeftPoint:origin]; + + // Show the popup. + if (![mPopupWin isVisible]) { + [browserWindow addChildWindow:mPopupWin ordered:NSWindowAbove]; + [mPopupWin orderFront:nil]; + } +} + +- (void)resizePopup +{ + // Don't resize if popup isn't visible. + if (![mPopupWin isVisible]) + return; + + if ([self visibleRows] == 0) { + [self closePopup]; + return; + } + + NSRect popupWinFrame = [mPopupWin frame]; + int tableHeight = (int)([mTableView rowHeight] + [mTableView intercellSpacing].height) * [self visibleRows]; + + popupWinFrame.origin.y += NSHeight(popupWinFrame) - tableHeight; + popupWinFrame.size.height = tableHeight; + + [mPopupWin setFrame:popupWinFrame display:YES]; +} + +- (void)closePopup +{ + // We can get -closePopup even if we didn't show it. + if ([mPopupWin isVisible]) { + [[mPopupWin parentWindow] removeChildWindow:mPopupWin]; + [mPopupWin orderOut:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; + } +} + +- (void)onRowClicked:(NSNotification *)aNote +{ + [mController popupSelected]; + [self closePopup]; +} + +- (int)visibleRows +{ + int minRows = [self rowCount]; + return (minRows < kFormFillMaxRows) ? minRows : kFormFillMaxRows; +} + +- (int)rowCount +{ + if (!mItems) + return 0; + + return [mItems count]; +} + +- (void)selectRow:(int)index +{ + if (index == -1) + [mTableView deselectAll:self]; + else { + [mTableView selectRowIndexes:[NSIndexSet indexSetWithIndex:index] byExtendingSelection:NO]; + [mTableView scrollRowToVisible:index]; + } +} + +- (int)selectedRow +{ + return [mTableView selectedRow]; +} + +- (NSString*)resultForRow:(int)index +{ + return [mItems objectAtIndex:index]; +} + +- (void)setItems:(NSArray*)items +{ + if (items != mItems) { + [mItems release]; + mItems = [items retain]; + } + + // Update the view any time we get new data + [mTableView noteNumberOfRowsChanged]; + [self resizePopup]; +} + +// methods for table view interaction +-(int)numberOfRowsInTableView:(NSTableView*)aTableView +{ + return [self rowCount]; +} + +-(id)tableView:(NSTableView*)aTableView objectValueForTableColumn:(NSTableColumn*)aTableColumn row:(int)aRowIndex +{ + return [self resultForRow:aRowIndex]; +} + +@end diff --git a/mozilla/camino/src/formfill/KeychainAutoCompleteSession.h b/mozilla/camino/src/formfill/KeychainAutoCompleteSession.h new file mode 100644 index 00000000000..8b3968be539 --- /dev/null +++ b/mozilla/camino/src/formfill/KeychainAutoCompleteSession.h @@ -0,0 +1,85 @@ +/* -*- 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 Camino code. + * + * The Initial Developer of the Original Code is + * Bryan Atwood + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Bryan Atwood + * + * 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 +#import "AutoCompleteUtils.h" + +#include "nsIDOMEventListener.h" +#include "nsCOMPtr.h" + +class nsIDOMHTMLInputElement; + +// +// KeychainAutoCompleteDOMListener +// +// Listens for password fill requests from the FormFillController when a +// username is entered. +class KeychainAutoCompleteDOMListener : public nsIDOMEventListener +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIDOMEVENTLISTENER + + void SetElements(nsIDOMHTMLInputElement* usernameElement, + nsIDOMHTMLInputElement* passwordElement); + +protected: + // Fills the password with the keychain data from the cached username input + // element. + void FillPassword(); + + // Pointers to the login input elements so that the form doesn't need to + // be searched when the same input element is attached again. + nsCOMPtr mUsernameElement; // strong + nsCOMPtr mPasswordElement; // strong +}; + +@interface KeychainAutoCompleteSession : NSObject +{ + KeychainAutoCompleteDOMListener* mDOMListener; // strong + NSMutableArray* mUsernames; // strong + NSString* mDefaultUser; // strong + + // Cache the username input element so that we don't reread the keychain for + // the same element. + nsIDOMHTMLInputElement* mUsernameElement; // weak +} + +- (BOOL)attachToInput:(nsIDOMHTMLInputElement*)usernameElement; + +@end diff --git a/mozilla/camino/src/formfill/KeychainAutoCompleteSession.mm b/mozilla/camino/src/formfill/KeychainAutoCompleteSession.mm new file mode 100644 index 00000000000..f05c64e0498 --- /dev/null +++ b/mozilla/camino/src/formfill/KeychainAutoCompleteSession.mm @@ -0,0 +1,369 @@ +/* -*- 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 Camino code. + * + * The Initial Developer of the Original Code is + * Bryan Atwood + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Bryan Atwood + * + * 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+Gecko.h" + +#import "KeychainAutoCompleteSession.h" +#import "KeychainItem.h" +#import "KeychainService.h" + +#include "nsIDocument.h" +#include "nsIDOMDocument.h" +#include "nsIDOMEvent.h" +#include "nsIDOMEventTarget.h"; +#include "nsIDOMHTMLInputElement.h"; + +static BOOL FindPasswordField(nsIDOMHTMLInputElement* inUsername, nsIDOMHTMLInputElement** outPassword); + +static void GetFormInfoForInput(nsIDOMHTMLInputElement* aElement, + NSString** host, + NSString** asciiHost, + PRInt32* port, + NSString** scheme); + +NS_IMPL_ISUPPORTS1(KeychainAutoCompleteDOMListener, nsIDOMEventListener) + +NS_IMETHODIMP KeychainAutoCompleteDOMListener::HandleEvent(nsIDOMEvent *aEvent) +{ + nsAutoString type; + aEvent->GetType(type); + if (type.EqualsLiteral("DOMAutoComplete")) { + // Don't fill password if autofilling has been disabled. + if(![[KeychainService instance] formPasswordFillIsEnabled]) + return NS_OK; + + nsCOMPtr target; + if (NS_FAILED(aEvent->GetTarget(getter_AddRefs(target)))) + return NS_OK; + + nsCOMPtr userElement = do_QueryInterface(target); + if (userElement && userElement == mUsernameElement) + FillPassword(); + } + + return NS_OK; +} + +void KeychainAutoCompleteDOMListener::SetElements(nsIDOMHTMLInputElement* usernameElement, + nsIDOMHTMLInputElement* passwordElement) +{ + mUsernameElement = usernameElement; + mPasswordElement = passwordElement; +} + +void KeychainAutoCompleteDOMListener::FillPassword() +{ + if (!mUsernameElement || !mPasswordElement) + return; + + // Get the entered username. + nsAutoString nsUsername; + mUsernameElement->GetValue(nsUsername); + NSString* username = [NSString stringWith_nsAString:nsUsername]; + + NSString* host; + NSString* asciiHost; + PRInt32 port; + NSString* scheme; + + GetFormInfoForInput(mUsernameElement, &host, &asciiHost, &port, &scheme); + + KeychainService* keychain = [KeychainService instance]; + KeychainItem* keychainEntry = [keychain findWebFormKeychainEntryForUsername:username + forHost:host + port:port + scheme:scheme]; + if (!keychainEntry && ![asciiHost isEqualToString:host]) { + keychainEntry = [keychain findWebFormKeychainEntryForUsername:username + forHost:asciiHost + port:port + scheme:scheme]; + } + if (!keychainEntry) + return; + + nsAutoString password; + [[keychainEntry password] assignTo_nsAString:password]; + + if (password.IsEmpty()) + return; + + mPasswordElement->SetValue(password); + + // Now that we have actually filled the password, cache the keychain entry. + nsCOMPtr domDoc; + nsresult rv = mUsernameElement->GetOwnerDocument(getter_AddRefs(domDoc)); + if (NS_FAILED(rv) || !domDoc) + return; + + nsCOMPtr doc (do_QueryInterface(domDoc)); + if (!doc) + return; + + nsIURI* docURL = doc->GetDocumentURI(); + if (!docURL) + return; + + nsCAutoString uriCAString; + rv = docURL->GetSpec(uriCAString); + if (NS_FAILED(rv)) + return; + + NSString* uri = [NSString stringWithCString:uriCAString.get()]; + if (uri) + [keychain cacheKeychainEntry:keychainEntry forKey:uri]; +} + +@implementation KeychainAutoCompleteSession + +- (id)init +{ + if ((self = [super init])) { + mUsernames = [[NSMutableArray alloc] init]; + mDOMListener = new KeychainAutoCompleteDOMListener(); + } + + return self; +} + +- (void)dealloc +{ + [mUsernames release]; + [mDefaultUser release]; + + if (mDOMListener) + delete mDOMListener; + + [super dealloc]; +} + +// +// attachToInput +// +// Sets the session to cache the usernames for a username element +// Returns NO if this element is not a valid username element and should +// be remembered by the calling function so that autocomplete requests are not sent. +- (BOOL)attachToInput:(nsIDOMHTMLInputElement*)usernameElement +{ + // Don't listen if element is empty or password fill is disabled. + KeychainService* keychain = [KeychainService instance]; + if (!usernameElement || ![keychain formPasswordFillIsEnabled]) + return NO; + + if (mUsernameElement == usernameElement) + return YES; + + // If there is no corresponding password, this element isn't listened to. + nsCOMPtr passwordElement; + if (!FindPasswordField(usernameElement, getter_AddRefs(passwordElement))) + return NO; + + // Get the host information so we can get the keychain entries below. + NSString* host; + NSString* asciiHost; + PRInt32 port; + NSString* scheme; + + GetFormInfoForInput(usernameElement, &host, &asciiHost, &port, &scheme); + + // If session is not cached, default to empty session. + [mUsernames removeAllObjects]; + [mDefaultUser release]; + mDefaultUser = nil; + mUsernameElement = usernameElement; + + // Cache all of the usernames in the object so that the keychain + // doesn't have to be read again. This should be a faster way to search and sort. + NSMutableArray* keychainEntries = + [NSMutableArray arrayWithArray:[keychain allWebFormKeychainItemsForHost:host port:port scheme:scheme]]; + + // And add the keychain items for the punycode host. + if (![asciiHost isEqualToString:host]) + [keychainEntries addObjectsFromArray:[keychain allWebFormKeychainItemsForHost:asciiHost port:port scheme:scheme]]; + + NSEnumerator* keychainEnumerator = [keychainEntries objectEnumerator]; + KeychainItem* item; + while ((item = [keychainEnumerator nextObject])) { + // Only add a username once since there may be duplicates. + NSString* username = [item username]; + if (![mUsernames containsObject:username]) + [mUsernames addObject:username]; + } + + // Get the default keychain and cache the username. + item = [keychain defaultFromKeychainItems:keychainEntries]; + if (item) + mDefaultUser = [[item username] copy]; + + // Sort usernames alphabetically. + [mUsernames sortUsingSelector:@selector(caseInsensitiveCompare:)]; + + // Listen for DOM form fill events so that the password can + // be autofilled when a username is entered. + mDOMListener->SetElements(usernameElement, passwordElement); + nsCOMPtr targ = do_QueryInterface(usernameElement); + targ->AddEventListener(NS_LITERAL_STRING("DOMAutoComplete"), mDOMListener, PR_FALSE); + + return YES; +} + +// +// startAutoCompleteWithSearch +// +// Function that takes a search string and returns all usernames that match it. +// Assume that if this function is called that formfilling is enabled. If it's +// not enabled, don't create the session in the first place. +// +-(void)startAutoCompleteWithSearch:(NSString*)searchString + previousResults:(AutoCompleteResults*)previousSearchResults + listener:(id)listener +{ + AutoCompleteResults* results = [[AutoCompleteResults alloc] init]; + [results setSearchString:searchString]; + + // determine if we can skip searching the whole list of usernames + // and only search through the previous search results. + bool searchPrevious = (previousSearchResults) ? [searchString hasPrefix:[previousSearchResults searchString]] : NO; + + NSEnumerator* usernameEnumerator; + if (searchPrevious) + usernameEnumerator = [[previousSearchResults matches] objectEnumerator]; + else + usernameEnumerator = [mUsernames objectEnumerator]; + + NSMutableArray* resultMatches = [[NSMutableArray alloc] init]; + + NSString* username; + bool searchStringIsEmpty = (searchString && [searchString length] > 0) ? NO : YES; + while ((username = [usernameEnumerator nextObject])) { + if (searchStringIsEmpty || [username hasPrefix:searchString]) { + [resultMatches addObject:username]; + + // Check for the default. + if ([username isEqualToString:mDefaultUser]) + [results setDefaultIndex:([resultMatches count]-1)]; + } + } + + [results setMatches:resultMatches]; + [resultMatches release]; + + [listener autoCompleteFoundResults:results]; + [results release]; +} + +@end + +// +// FindPasswordField +// +// Given an HTML input element for username entry, return the corresponding password field +// or set the password input element to nsnull if there isn't one. Return YES on success. +// +BOOL FindPasswordField(nsIDOMHTMLInputElement* inUsername, nsIDOMHTMLInputElement** outPassword) +{ + if (!inUsername) + return NO; + + // Get the form node for scanning username/password pair. + nsCOMPtr formElement; + nsresult rv = inUsername->GetForm(getter_AddRefs(formElement)); + if (NS_FAILED(rv) || !formElement) + return NO; + + // Check if the input element is a username field with a password field. + nsCOMPtr usernameElement; + rv = FindUsernamePasswordFields(formElement, + getter_AddRefs(usernameElement), + outPassword, + PR_TRUE); + // Return a null password if the password was not found or if the username + // field in the form is not the same as the input. + if (NS_FAILED(rv) || usernameElement != inUsername) + return NO; + + return YES; +} + +static void GetFormInfoForInput(nsIDOMHTMLInputElement* aElement, + NSString** host, + NSString** asciiHost, + PRInt32* port, + NSString** scheme) +{ + if (!aElement) + return; + *host = nil; + *asciiHost = nil; + *port = -1; + *scheme = nil; + + nsCOMPtr domDoc; + nsresult rv = aElement->GetOwnerDocument(getter_AddRefs(domDoc)); + if (NS_FAILED(rv)) + return; + + nsCOMPtr doc = do_QueryInterface(domDoc); + if (!doc) + return; + + nsIURI* docURL = doc->GetDocumentURI(); + if (!docURL) + return; + + nsCAutoString hostCAString; + rv = docURL->GetHost(hostCAString); + if (NS_FAILED(rv)) + return; + + *host = [NSString stringWithCString:hostCAString.get()]; + + // Get the host in punycode for keychain use. + nsCAutoString asciiHostCAString; + rv = docURL->GetAsciiHost(asciiHostCAString); + *asciiHost = NS_SUCCEEDED(rv) ? [NSString stringWithCString:asciiHostCAString.get()] + : *host; + + docURL->GetPort(port); + + nsCAutoString schemeCAString; + rv = docURL->GetScheme(schemeCAString); + if (NS_FAILED(rv)) + return; + + *scheme = [NSString stringWithCString:schemeCAString.get()]; +}