1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5/* 6 * Form Autofill content process module. 7 */ 8 9/* eslint-disable no-use-before-define */ 10 11"use strict"; 12 13var EXPORTED_SYMBOLS = ["FormAutofillContent"]; 14 15const Cm = Components.manager; 16 17ChromeUtils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); 18ChromeUtils.import("resource://gre/modules/Services.jsm"); 19ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); 20ChromeUtils.import("resource://formautofill/FormAutofillUtils.jsm"); 21 22ChromeUtils.defineModuleGetter(this, "AddressResult", 23 "resource://formautofill/ProfileAutoCompleteResult.jsm"); 24ChromeUtils.defineModuleGetter(this, "CreditCardResult", 25 "resource://formautofill/ProfileAutoCompleteResult.jsm"); 26ChromeUtils.defineModuleGetter(this, "FormAutofillHandler", 27 "resource://formautofill/FormAutofillHandler.jsm"); 28ChromeUtils.defineModuleGetter(this, "FormLikeFactory", 29 "resource://gre/modules/FormLikeFactory.jsm"); 30ChromeUtils.defineModuleGetter(this, "InsecurePasswordUtils", 31 "resource://gre/modules/InsecurePasswordUtils.jsm"); 32 33const formFillController = Cc["@mozilla.org/satchel/form-fill-controller;1"] 34 .getService(Ci.nsIFormFillController); 35const autocompleteController = Cc["@mozilla.org/autocomplete/controller;1"] 36 .getService(Ci.nsIAutoCompleteController); 37 38const {ADDRESSES_COLLECTION_NAME, CREDITCARDS_COLLECTION_NAME, FIELD_STATES} = FormAutofillUtils; 39 40// Register/unregister a constructor as a factory. 41function AutocompleteFactory() {} 42AutocompleteFactory.prototype = { 43 register(targetConstructor) { 44 let proto = targetConstructor.prototype; 45 this._classID = proto.classID; 46 47 let factory = XPCOMUtils._getFactory(targetConstructor); 48 this._factory = factory; 49 50 let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); 51 registrar.registerFactory(proto.classID, proto.classDescription, 52 proto.contractID, factory); 53 54 if (proto.classID2) { 55 this._classID2 = proto.classID2; 56 registrar.registerFactory(proto.classID2, proto.classDescription, 57 proto.contractID2, factory); 58 } 59 }, 60 61 unregister() { 62 let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); 63 registrar.unregisterFactory(this._classID, this._factory); 64 if (this._classID2) { 65 registrar.unregisterFactory(this._classID2, this._factory); 66 } 67 this._factory = null; 68 }, 69}; 70 71 72/** 73 * @constructor 74 * 75 * @implements {nsIAutoCompleteSearch} 76 */ 77function AutofillProfileAutoCompleteSearch() { 78 FormAutofillUtils.defineLazyLogGetter(this, "AutofillProfileAutoCompleteSearch"); 79} 80AutofillProfileAutoCompleteSearch.prototype = { 81 classID: Components.ID("4f9f1e4c-7f2c-439e-9c9e-566b68bc187d"), 82 contractID: "@mozilla.org/autocomplete/search;1?name=autofill-profiles", 83 classDescription: "AutofillProfileAutoCompleteSearch", 84 QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteSearch]), 85 86 // Begin nsIAutoCompleteSearch implementation 87 88 /** 89 * Searches for a given string and notifies a listener (either synchronously 90 * or asynchronously) of the result 91 * 92 * @param {string} searchString the string to search for 93 * @param {string} searchParam 94 * @param {Object} previousResult a previous result to use for faster searchinig 95 * @param {Object} listener the listener to notify when the search is complete 96 */ 97 startSearch(searchString, searchParam, previousResult, listener) { 98 let {activeInput, activeSection, activeFieldDetail, savedFieldNames} = FormAutofillContent; 99 this.forceStop = false; 100 101 this.log.debug("startSearch: for", searchString, "with input", activeInput); 102 103 let isAddressField = FormAutofillUtils.isAddressField(activeFieldDetail.fieldName); 104 let isInputAutofilled = activeFieldDetail.state == FIELD_STATES.AUTO_FILLED; 105 let allFieldNames = activeSection.allFieldNames; 106 let filledRecordGUID = activeSection.filledRecordGUID; 107 let searchPermitted = isAddressField ? 108 FormAutofillUtils.isAutofillAddressesEnabled : 109 FormAutofillUtils.isAutofillCreditCardsEnabled; 110 let AutocompleteResult = isAddressField ? AddressResult : CreditCardResult; 111 let pendingSearchResult = null; 112 113 ProfileAutocomplete.lastProfileAutoCompleteFocusedInput = activeInput; 114 // Fallback to form-history if ... 115 // - specified autofill feature is pref off. 116 // - no profile can fill the currently-focused input. 117 // - the current form has already been populated. 118 // - (address only) less than 3 inputs are covered by all saved fields in the storage. 119 if (!searchPermitted || !savedFieldNames.has(activeFieldDetail.fieldName) || 120 (!isInputAutofilled && filledRecordGUID) || (isAddressField && 121 allFieldNames.filter(field => savedFieldNames.has(field)).length < FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD)) { 122 if (activeInput.autocomplete == "off") { 123 // Create a dummy result as an empty search result. 124 pendingSearchResult = new AutocompleteResult("", "", [], [], {}); 125 } else { 126 pendingSearchResult = new Promise(resolve => { 127 let formHistory = Cc["@mozilla.org/autocomplete/search;1?name=form-history"] 128 .createInstance(Ci.nsIAutoCompleteSearch); 129 formHistory.startSearch(searchString, searchParam, previousResult, { 130 onSearchResult: (_, result) => resolve(result), 131 }); 132 }); 133 } 134 } else if (isInputAutofilled) { 135 pendingSearchResult = new AutocompleteResult(searchString, "", [], [], {isInputAutofilled}); 136 } else { 137 let infoWithoutElement = {...activeFieldDetail}; 138 delete infoWithoutElement.elementWeakRef; 139 140 let data = { 141 collectionName: isAddressField ? ADDRESSES_COLLECTION_NAME : CREDITCARDS_COLLECTION_NAME, 142 info: infoWithoutElement, 143 searchString, 144 }; 145 146 pendingSearchResult = this._getRecords(data).then((records) => { 147 if (this.forceStop) { 148 return null; 149 } 150 // Sort addresses by timeLastUsed for showing the lastest used address at top. 151 records.sort((a, b) => b.timeLastUsed - a.timeLastUsed); 152 153 let adaptedRecords = activeSection.getAdaptedProfiles(records); 154 let handler = FormAutofillContent.activeHandler; 155 let isSecure = InsecurePasswordUtils.isFormSecure(handler.form); 156 157 return new AutocompleteResult(searchString, 158 activeFieldDetail.fieldName, 159 allFieldNames, 160 adaptedRecords, 161 {isSecure, isInputAutofilled}); 162 }); 163 } 164 165 Promise.resolve(pendingSearchResult).then((result) => { 166 listener.onSearchResult(this, result); 167 ProfileAutocomplete.lastProfileAutoCompleteResult = result; 168 // Reset AutoCompleteController's state at the end of startSearch to ensure that 169 // none of form autofill result will be cached in other places and make the 170 // result out of sync. 171 autocompleteController.resetInternalState(); 172 }); 173 }, 174 175 /** 176 * Stops an asynchronous search that is in progress 177 */ 178 stopSearch() { 179 ProfileAutocomplete.lastProfileAutoCompleteResult = null; 180 this.forceStop = true; 181 }, 182 183 /** 184 * Get the records from parent process for AutoComplete result. 185 * 186 * @private 187 * @param {Object} data 188 * Parameters for querying the corresponding result. 189 * @param {string} data.collectionName 190 * The name used to specify which collection to retrieve records. 191 * @param {string} data.searchString 192 * The typed string for filtering out the matched records. 193 * @param {string} data.info 194 * The input autocomplete property's information. 195 * @returns {Promise} 196 * Promise that resolves when addresses returned from parent process. 197 */ 198 _getRecords(data) { 199 this.log.debug("_getRecords with data:", data); 200 return new Promise((resolve) => { 201 Services.cpmm.addMessageListener("FormAutofill:Records", function getResult(result) { 202 Services.cpmm.removeMessageListener("FormAutofill:Records", getResult); 203 resolve(result.data); 204 }); 205 206 Services.cpmm.sendAsyncMessage("FormAutofill:GetRecords", data); 207 }); 208 }, 209}; 210 211let ProfileAutocomplete = { 212 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), 213 214 lastProfileAutoCompleteResult: null, 215 lastProfileAutoCompleteFocusedInput: null, 216 _registered: false, 217 _factory: null, 218 219 ensureRegistered() { 220 if (this._registered) { 221 return; 222 } 223 224 FormAutofillUtils.defineLazyLogGetter(this, "ProfileAutocomplete"); 225 this.log.debug("ensureRegistered"); 226 this._factory = new AutocompleteFactory(); 227 this._factory.register(AutofillProfileAutoCompleteSearch); 228 this._registered = true; 229 230 Services.obs.addObserver(this, "autocomplete-will-enter-text"); 231 }, 232 233 ensureUnregistered() { 234 if (!this._registered) { 235 return; 236 } 237 238 this.log.debug("ensureUnregistered"); 239 this._factory.unregister(); 240 this._factory = null; 241 this._registered = false; 242 this._lastAutoCompleteResult = null; 243 244 Services.obs.removeObserver(this, "autocomplete-will-enter-text"); 245 }, 246 247 observe(subject, topic, data) { 248 switch (topic) { 249 case "autocomplete-will-enter-text": { 250 if (!FormAutofillContent.activeInput) { 251 // The observer notification is for autocomplete in a different process. 252 break; 253 } 254 this._fillFromAutocompleteRow(FormAutofillContent.activeInput); 255 break; 256 } 257 } 258 }, 259 260 _frameMMFromWindow(contentWindow) { 261 return contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) 262 .getInterface(Ci.nsIDocShell) 263 .QueryInterface(Ci.nsIInterfaceRequestor) 264 .getInterface(Ci.nsIContentFrameMessageManager); 265 }, 266 267 _getSelectedIndex(contentWindow) { 268 let mm = this._frameMMFromWindow(contentWindow); 269 let selectedIndexResult = mm.sendSyncMessage("FormAutoComplete:GetSelectedIndex", {}); 270 if (selectedIndexResult.length != 1 || !Number.isInteger(selectedIndexResult[0])) { 271 throw new Error("Invalid autocomplete selectedIndex"); 272 } 273 274 return selectedIndexResult[0]; 275 }, 276 277 _fillFromAutocompleteRow(focusedInput) { 278 this.log.debug("_fillFromAutocompleteRow:", focusedInput); 279 let formDetails = FormAutofillContent.activeFormDetails; 280 if (!formDetails) { 281 // The observer notification is for a different frame. 282 return; 283 } 284 285 let selectedIndex = this._getSelectedIndex(focusedInput.ownerGlobal); 286 if (selectedIndex == -1 || 287 !this.lastProfileAutoCompleteResult || 288 this.lastProfileAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile") { 289 return; 290 } 291 292 let profile = JSON.parse(this.lastProfileAutoCompleteResult.getCommentAt(selectedIndex)); 293 294 FormAutofillContent.activeHandler.autofillFormFields(profile); 295 }, 296 297 _clearProfilePreview() { 298 if (!this.lastProfileAutoCompleteFocusedInput || !FormAutofillContent.activeSection) { 299 return; 300 } 301 302 FormAutofillContent.activeSection.clearPreviewedFormFields(); 303 }, 304 305 _previewSelectedProfile(selectedIndex) { 306 if (!FormAutofillContent.activeInput || !FormAutofillContent.activeFormDetails) { 307 // The observer notification is for a different process/frame. 308 return; 309 } 310 311 if (!this.lastProfileAutoCompleteResult || 312 this.lastProfileAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile") { 313 return; 314 } 315 316 let profile = JSON.parse(this.lastProfileAutoCompleteResult.getCommentAt(selectedIndex)); 317 FormAutofillContent.activeSection.previewFormFields(profile); 318 }, 319}; 320 321/** 322 * Handles content's interactions for the process. 323 * 324 * NOTE: Declares it by "var" to make it accessible in unit tests. 325 */ 326var FormAutofillContent = { 327 QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]), 328 /** 329 * @type {WeakMap} mapping FormLike root HTML elements to FormAutofillHandler objects. 330 */ 331 _formsDetails: new WeakMap(), 332 333 /** 334 * @type {Set} Set of the fields with usable values in any saved profile. 335 */ 336 savedFieldNames: null, 337 338 /** 339 * @type {Object} The object where to store the active items, e.g. element, 340 * handler, section, and field detail. 341 */ 342 _activeItems: {}, 343 344 init() { 345 FormAutofillUtils.defineLazyLogGetter(this, "FormAutofillContent"); 346 347 Services.cpmm.addMessageListener("FormAutofill:enabledStatus", this); 348 Services.cpmm.addMessageListener("FormAutofill:savedFieldNames", this); 349 Services.obs.addObserver(this, "earlyformsubmit"); 350 351 let autofillEnabled = Services.cpmm.initialProcessData.autofillEnabled; 352 // If storage hasn't be initialized yet autofillEnabled is undefined but we need to ensure 353 // autocomplete is registered before the focusin so register it in this case as long as the 354 // pref is true. 355 let shouldEnableAutofill = autofillEnabled === undefined && 356 (FormAutofillUtils.isAutofillAddressesEnabled || 357 FormAutofillUtils.isAutofillCreditCardsEnabled); 358 if (autofillEnabled || shouldEnableAutofill) { 359 ProfileAutocomplete.ensureRegistered(); 360 } 361 362 this.savedFieldNames = 363 Services.cpmm.initialProcessData.autofillSavedFieldNames; 364 }, 365 366 /** 367 * Send the profile to parent for doorhanger and storage saving/updating. 368 * 369 * @param {Object} profile Submitted form's address/creditcard guid and record. 370 * @param {Object} domWin Current content window. 371 * @param {int} timeStartedFillingMS Time of form filling started. 372 */ 373 _onFormSubmit(profile, domWin, timeStartedFillingMS) { 374 let mm = this._messageManagerFromWindow(domWin); 375 mm.sendAsyncMessage("FormAutofill:OnFormSubmit", 376 {profile, timeStartedFillingMS}); 377 }, 378 379 /** 380 * Handle earlyformsubmit event and early return when: 381 * 1. In private browsing mode. 382 * 2. Could not map any autofill handler by form element. 383 * 3. Number of filled fields is less than autofill threshold 384 * 385 * @param {HTMLElement} formElement Root element which receives earlyformsubmit event. 386 * @param {Object} domWin Content window 387 * @returns {boolean} Should always return true so form submission isn't canceled. 388 */ 389 notify(formElement, domWin) { 390 this.log.debug("Notifying form early submission"); 391 392 if (!FormAutofillUtils.isAutofillEnabled) { 393 this.log.debug("Form Autofill is disabled"); 394 return true; 395 } 396 397 if (domWin && PrivateBrowsingUtils.isContentWindowPrivate(domWin)) { 398 this.log.debug("Ignoring submission in a private window"); 399 return true; 400 } 401 402 let handler = this._formsDetails.get(formElement); 403 if (!handler) { 404 this.log.debug("Form element could not map to an existing handler"); 405 return true; 406 } 407 408 let records = handler.createRecords(); 409 if (!Object.values(records).some(typeRecords => typeRecords.length)) { 410 return true; 411 } 412 413 this._onFormSubmit(records, domWin, handler.timeStartedFillingMS); 414 return true; 415 }, 416 417 receiveMessage({name, data}) { 418 switch (name) { 419 case "FormAutofill:enabledStatus": { 420 if (data) { 421 ProfileAutocomplete.ensureRegistered(); 422 } else { 423 ProfileAutocomplete.ensureUnregistered(); 424 } 425 break; 426 } 427 case "FormAutofill:savedFieldNames": { 428 this.savedFieldNames = data; 429 } 430 } 431 }, 432 433 /** 434 * Get the form's handler from cache which is created after page identified. 435 * 436 * @param {HTMLInputElement} element Focused input which triggered profile searching 437 * @returns {Array<Object>|null} 438 * Return target form's handler from content cache 439 * (or return null if the information is not found in the cache). 440 * 441 */ 442 _getFormHandler(element) { 443 if (!element) { 444 return null; 445 } 446 let rootElement = FormLikeFactory.findRootForField(element); 447 return this._formsDetails.get(rootElement); 448 }, 449 450 /** 451 * Get the active form's information from cache which is created after page 452 * identified. 453 * 454 * @returns {Array<Object>|null} 455 * Return target form's information from content cache 456 * (or return null if the information is not found in the cache). 457 * 458 */ 459 get activeFormDetails() { 460 let formHandler = this.activeHandler; 461 return formHandler ? formHandler.fieldDetails : null; 462 }, 463 464 /** 465 * All active items should be updated according the active element of 466 * `formFillController.focusedInput`. All of them including element, 467 * handler, section, and field detail, can be retrieved by their own getters. 468 * 469 * @param {HTMLElement|null} element The active item should be updated based 470 * on this or `formFillController.focusedInput` will be taken. 471 */ 472 updateActiveInput(element) { 473 element = element || formFillController.focusedInput; 474 if (!element) { 475 this._activeItems = {}; 476 return; 477 } 478 let handler = this._getFormHandler(element); 479 if (handler) { 480 handler.focusedInput = element; 481 } 482 this._activeItems = { 483 handler, 484 elementWeakRef: Cu.getWeakReference(element), 485 section: handler ? handler.activeSection : null, 486 fieldDetail: null, 487 }; 488 }, 489 490 get activeInput() { 491 let elementWeakRef = this._activeItems.elementWeakRef; 492 return elementWeakRef ? elementWeakRef.get() : null; 493 }, 494 495 get activeHandler() { 496 return this._activeItems.handler; 497 }, 498 499 get activeSection() { 500 return this._activeItems.section; 501 }, 502 503 /** 504 * Get the active input's information from cache which is created after page 505 * identified. 506 * 507 * @returns {Object|null} 508 * Return the active input's information that cloned from content cache 509 * (or return null if the information is not found in the cache). 510 */ 511 get activeFieldDetail() { 512 if (!this._activeItems.fieldDetail) { 513 let formDetails = this.activeFormDetails; 514 if (!formDetails) { 515 return null; 516 } 517 for (let detail of formDetails) { 518 let detailElement = detail.elementWeakRef.get(); 519 if (detailElement && this.activeInput == detailElement) { 520 this._activeItems.fieldDetail = detail; 521 break; 522 } 523 } 524 } 525 return this._activeItems.fieldDetail; 526 }, 527 528 identifyAutofillFields(element) { 529 this.log.debug("identifyAutofillFields:", "" + element.ownerDocument.location); 530 531 if (!this.savedFieldNames) { 532 this.log.debug("identifyAutofillFields: savedFieldNames are not known yet"); 533 Services.cpmm.sendAsyncMessage("FormAutofill:InitStorage"); 534 } 535 536 let formHandler = this._getFormHandler(element); 537 if (!formHandler) { 538 let formLike = FormLikeFactory.createFromField(element); 539 formHandler = new FormAutofillHandler(formLike); 540 } else if (!formHandler.updateFormIfNeeded(element)) { 541 this.log.debug("No control is removed or inserted since last collection."); 542 return; 543 } 544 545 let validDetails = formHandler.collectFormFields(); 546 547 this._formsDetails.set(formHandler.form.rootElement, formHandler); 548 this.log.debug("Adding form handler to _formsDetails:", formHandler); 549 550 validDetails.forEach(detail => 551 this._markAsAutofillField(detail.elementWeakRef.get()) 552 ); 553 }, 554 555 clearForm() { 556 let focusedInput = this.activeInput || ProfileAutocomplete._lastAutoCompleteFocusedInput; 557 if (!focusedInput) { 558 return; 559 } 560 561 this.activeSection.clearPopulatedForm(); 562 }, 563 564 previewProfile(doc) { 565 let docWin = doc.ownerGlobal; 566 let selectedIndex = ProfileAutocomplete._getSelectedIndex(docWin); 567 let lastAutoCompleteResult = ProfileAutocomplete.lastProfileAutoCompleteResult; 568 let focusedInput = this.activeInput; 569 let mm = this._messageManagerFromWindow(docWin); 570 571 if (selectedIndex === -1 || 572 !focusedInput || 573 !lastAutoCompleteResult || 574 lastAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile") { 575 mm.sendAsyncMessage("FormAutofill:UpdateWarningMessage", {}); 576 577 ProfileAutocomplete._clearProfilePreview(); 578 } else { 579 let focusedInputDetails = this.activeFieldDetail; 580 let profile = JSON.parse(lastAutoCompleteResult.getCommentAt(selectedIndex)); 581 let allFieldNames = FormAutofillContent.activeSection.allFieldNames; 582 let profileFields = allFieldNames.filter(fieldName => !!profile[fieldName]); 583 584 let focusedCategory = FormAutofillUtils.getCategoryFromFieldName(focusedInputDetails.fieldName); 585 let categories = FormAutofillUtils.getCategoriesFromFieldNames(profileFields); 586 mm.sendAsyncMessage("FormAutofill:UpdateWarningMessage", { 587 focusedCategory, 588 categories, 589 }); 590 591 ProfileAutocomplete._previewSelectedProfile(selectedIndex); 592 } 593 }, 594 595 onPopupClosed() { 596 ProfileAutocomplete._clearProfilePreview(); 597 }, 598 599 _markAsAutofillField(field) { 600 // Since Form Autofill popup is only for input element, any non-Input 601 // element should be excluded here. 602 if (!field || !(field instanceof Ci.nsIDOMHTMLInputElement)) { 603 return; 604 } 605 606 formFillController.markAsAutofillField(field); 607 }, 608 609 _messageManagerFromWindow(win) { 610 return win.QueryInterface(Ci.nsIInterfaceRequestor) 611 .getInterface(Ci.nsIWebNavigation) 612 .QueryInterface(Ci.nsIDocShell) 613 .QueryInterface(Ci.nsIInterfaceRequestor) 614 .getInterface(Ci.nsIContentFrameMessageManager); 615 }, 616 617 _onKeyDown(e) { 618 let lastAutoCompleteResult = ProfileAutocomplete.lastProfileAutoCompleteResult; 619 let focusedInput = FormAutofillContent.activeInput; 620 621 if (e.keyCode != e.DOM_VK_RETURN || !lastAutoCompleteResult || 622 !focusedInput || focusedInput != ProfileAutocomplete.lastProfileAutoCompleteFocusedInput) { 623 return; 624 } 625 626 let selectedIndex = ProfileAutocomplete._getSelectedIndex(e.target.ownerGlobal); 627 let selectedRowStyle = lastAutoCompleteResult.getStyleAt(selectedIndex); 628 focusedInput.addEventListener("DOMAutoComplete", () => { 629 if (selectedRowStyle == "autofill-footer") { 630 Services.cpmm.sendAsyncMessage("FormAutofill:OpenPreferences"); 631 } else if (selectedRowStyle == "autofill-clear-button") { 632 FormAutofillContent.clearForm(); 633 } 634 }, {once: true}); 635 }, 636}; 637 638 639FormAutofillContent.init(); 640