GeckoViewAutocomplete.sys.mjs (20580B)
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 file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 EventDispatcher: "resource://gre/modules/Messaging.sys.mjs", 11 GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs", 12 AddressRecord: "resource://gre/modules/shared/AddressRecord.sys.mjs", 13 CreditCardRecord: "resource://gre/modules/shared/CreditCardRecord.sys.mjs", 14 }); 15 16 ChromeUtils.defineLazyGetter(lazy, "LoginInfo", () => 17 Components.Constructor( 18 "@mozilla.org/login-manager/loginInfo;1", 19 "nsILoginInfo", 20 "init" 21 ) 22 ); 23 24 export class LoginEntry { 25 constructor({ 26 origin, 27 formActionOrigin, 28 httpRealm, 29 username, 30 password, 31 guid, 32 timeCreated, 33 timeLastUsed, 34 timePasswordChanged, 35 timesUsed, 36 }) { 37 this.origin = origin ?? ""; 38 this.formActionOrigin = formActionOrigin ?? null; 39 this.httpRealm = httpRealm ?? null; 40 this.username = username ?? ""; 41 this.password = password ?? ""; 42 43 // Metadata. 44 this.guid = guid ?? null; 45 // TODO: Not supported by GV. 46 this.timeCreated = timeCreated ?? null; 47 this.timeLastUsed = timeLastUsed ?? null; 48 this.timePasswordChanged = timePasswordChanged ?? null; 49 this.timesUsed = timesUsed ?? null; 50 } 51 52 toLoginInfo() { 53 const info = new lazy.LoginInfo( 54 this.origin, 55 this.formActionOrigin, 56 this.httpRealm, 57 this.username, 58 this.password 59 ); 60 61 // Metadata. 62 info.QueryInterface(Ci.nsILoginMetaInfo); 63 info.guid = this.guid; 64 info.timeCreated = this.timeCreated; 65 info.timeLastUsed = this.timeLastUsed; 66 info.timePasswordChanged = this.timePasswordChanged; 67 info.timesUsed = this.timesUsed; 68 69 return info; 70 } 71 72 static parse(aObj) { 73 const entry = new LoginEntry({}); 74 Object.assign(entry, aObj); 75 76 return entry; 77 } 78 79 static fromLoginInfo(aInfo) { 80 const entry = new LoginEntry({}); 81 entry.origin = aInfo.origin; 82 entry.formActionOrigin = aInfo.formActionOrigin; 83 entry.httpRealm = aInfo.httpRealm; 84 entry.username = aInfo.username; 85 entry.password = aInfo.password; 86 87 // Metadata. 88 aInfo.QueryInterface(Ci.nsILoginMetaInfo); 89 entry.guid = aInfo.guid; 90 entry.timeCreated = aInfo.timeCreated; 91 entry.timeLastUsed = aInfo.timeLastUsed; 92 entry.timePasswordChanged = aInfo.timePasswordChanged; 93 entry.timesUsed = aInfo.timesUsed; 94 95 return entry; 96 } 97 } 98 99 export class Address { 100 constructor({ 101 name, 102 givenName, 103 additionalName, 104 familyName, 105 organization, 106 streetAddress, 107 addressLevel1, 108 addressLevel2, 109 addressLevel3, 110 postalCode, 111 country, 112 tel, 113 email, 114 guid, 115 timeCreated, 116 timeLastUsed, 117 timeLastModified, 118 timesUsed, 119 version, 120 }) { 121 this.name = name ?? ""; 122 this.givenName = givenName ?? ""; 123 this.additionalName = additionalName ?? ""; 124 this.familyName = familyName ?? ""; 125 this.organization = organization ?? ""; 126 this.streetAddress = streetAddress ?? ""; 127 this.addressLevel1 = addressLevel1 ?? ""; 128 this.addressLevel2 = addressLevel2 ?? ""; 129 this.addressLevel3 = addressLevel3 ?? ""; 130 this.postalCode = postalCode ?? ""; 131 this.country = country ?? ""; 132 this.tel = tel ?? ""; 133 this.email = email ?? ""; 134 135 // Metadata. 136 this.guid = guid ?? null; 137 // TODO: Not supported by GV. 138 this.timeCreated = timeCreated ?? null; 139 this.timeLastUsed = timeLastUsed ?? null; 140 this.timeLastModified = timeLastModified ?? null; 141 this.timesUsed = timesUsed ?? null; 142 this.version = version ?? null; 143 } 144 145 isValid() { 146 return ( 147 (this.name ?? this.givenName ?? this.familyName) !== "" && 148 this.streetAddress !== "" && 149 this.postalCode !== "" 150 ); 151 } 152 153 static fromGecko(aObj) { 154 return new Address({ 155 version: aObj.version, 156 name: aObj.name, 157 givenName: aObj["given-name"], 158 additionalName: aObj["additional-name"], 159 familyName: aObj["family-name"], 160 organization: aObj.organization, 161 streetAddress: aObj["street-address"], 162 addressLevel1: aObj["address-level1"], 163 addressLevel2: aObj["address-level2"], 164 addressLevel3: aObj["address-level3"], 165 postalCode: aObj["postal-code"], 166 country: aObj.country, 167 tel: aObj.tel, 168 email: aObj.email, 169 guid: aObj.guid, 170 timeCreated: aObj.timeCreated, 171 timeLastUsed: aObj.timeLastUsed, 172 timeLastModified: aObj.timeLastModified, 173 timesUsed: aObj.timesUsed, 174 }); 175 } 176 177 static parse(aObj) { 178 const entry = new Address({}); 179 Object.assign(entry, aObj); 180 181 return entry; 182 } 183 184 toGecko() { 185 let address = { 186 version: this.version, 187 name: this.name, 188 organization: this.organization, 189 "street-address": this.streetAddress, 190 "address-level1": this.addressLevel1, 191 "address-level2": this.addressLevel2, 192 "address-level3": this.addressLevel3, 193 "postal-code": this.postalCode, 194 country: this.country, 195 tel: this.tel, 196 email: this.email, 197 guid: this.guid, 198 ...(this.givenName && { 199 "given-name": this.givenName, 200 "additional-name": this.additionalName, 201 "family-name": this.familyName, 202 }), 203 }; 204 205 lazy.AddressRecord.computeFields(address); 206 207 return address; 208 } 209 } 210 211 export class CreditCard { 212 constructor({ 213 name, 214 number, 215 expMonth, 216 expYear, 217 type, 218 guid, 219 timeCreated, 220 timeLastUsed, 221 timeLastModified, 222 timesUsed, 223 version, 224 }) { 225 this.name = name ?? ""; 226 this.number = number ?? ""; 227 this.expMonth = expMonth ?? ""; 228 this.expYear = expYear ?? ""; 229 this.type = type ?? ""; 230 231 // Metadata. 232 this.guid = guid ?? null; 233 // TODO: Not supported by GV. 234 this.timeCreated = timeCreated ?? null; 235 this.timeLastUsed = timeLastUsed ?? null; 236 this.timeLastModified = timeLastModified ?? null; 237 this.timesUsed = timesUsed ?? null; 238 this.version = version ?? null; 239 } 240 241 isValid() { 242 return this.number !== ""; 243 } 244 245 static fromGecko(aObj) { 246 return new CreditCard({ 247 version: aObj.version, 248 name: aObj["cc-name"], 249 number: aObj["cc-number"], 250 expMonth: aObj["cc-exp-month"]?.toString(), 251 expYear: aObj["cc-exp-year"]?.toString(), 252 type: aObj["cc-type"], 253 guid: aObj.guid, 254 timeCreated: aObj.timeCreated, 255 timeLastUsed: aObj.timeLastUsed, 256 timeLastModified: aObj.timeLastModified, 257 timesUsed: aObj.timesUsed, 258 }); 259 } 260 261 static parse(aObj) { 262 const entry = new CreditCard({}); 263 Object.assign(entry, aObj); 264 265 return entry; 266 } 267 268 toGecko() { 269 let creditCard = { 270 version: this.version, 271 "cc-name": this.name, 272 "cc-number": this.number, 273 "cc-exp-month": this.expMonth, 274 "cc-exp-year": this.expYear, 275 "cc-type": this.type, 276 guid: this.guid, 277 }; 278 279 lazy.CreditCardRecord.computeFields(creditCard); 280 281 return creditCard; 282 } 283 } 284 285 export class SelectOption { 286 // Sync with Autocomplete.SelectOption.Hint in Autocomplete.java. 287 static Hint = { 288 NONE: 0, 289 GENERATED: 1 << 0, 290 INSECURE_FORM: 1 << 1, 291 DUPLICATE_USERNAME: 1 << 2, 292 MATCHING_ORIGIN: 1 << 3, 293 }; 294 295 constructor({ value, hint }) { 296 this.value = value ?? null; 297 this.hint = hint ?? SelectOption.Hint.NONE; 298 } 299 } 300 301 // Sync with Autocomplete.UsedField in Autocomplete.java. 302 const UsedField = { PASSWORD: 1 }; 303 304 export const GeckoViewAutocomplete = { 305 /** current opened prompt */ 306 _prompt: null, 307 308 /** 309 * Delegates login entry fetching for the given domain to the attached 310 * LoginStorage GeckoView delegate. 311 * 312 * @param aDomain 313 * The domain string to fetch login entries for. If null, all logins 314 * will be fetched. 315 * @return {Promise} 316 * Resolves with an array of login objects or null. 317 * Rejected if no delegate is attached. 318 * Login object string properties: 319 * { guid, origin, formActionOrigin, httpRealm, username, password } 320 */ 321 fetchLogins(aDomain = null) { 322 debug`fetchLogins for ${aDomain ?? "All domains"}`; 323 324 return lazy.EventDispatcher.instance.sendRequestForResult({ 325 type: "GeckoView:Autocomplete:Fetch:Login", 326 domain: aDomain, 327 }); 328 }, 329 330 /** 331 * Delegates credit card entry fetching to the attached LoginStorage 332 * GeckoView delegate. 333 * 334 * @return {Promise} 335 * Resolves with an array of credit card objects or null. 336 * Rejected if no delegate is attached. 337 * Login object string properties: 338 * { guid, name, number, expMonth, expYear, type } 339 */ 340 fetchCreditCards() { 341 debug`fetchCreditCards`; 342 343 return lazy.EventDispatcher.instance.sendRequestForResult({ 344 type: "GeckoView:Autocomplete:Fetch:CreditCard", 345 }); 346 }, 347 348 /** 349 * Delegates address entry fetching to the attached LoginStorage 350 * GeckoView delegate. 351 * 352 * @return {Promise} 353 * Resolves with an array of address objects or null. 354 * Rejected if no delegate is attached. 355 * Login object string properties: 356 * { guid, name, givenName, additionalName, familyName, 357 * organization, streetAddress, addressLevel1, addressLevel2, 358 * addressLevel3, postalCode, country, tel, email } 359 */ 360 fetchAddresses() { 361 debug`fetchAddresses`; 362 363 return lazy.EventDispatcher.instance.sendRequestForResult({ 364 type: "GeckoView:Autocomplete:Fetch:Address", 365 }); 366 }, 367 368 /** 369 * Delegates credit card entry saving to the attached LoginStorage GeckoView delegate. 370 * Call this when a new or modified credit card entry has been submitted. 371 * 372 * @param aCreditCard The {CreditCard} to be saved. 373 */ 374 onCreditCardSave(aCreditCard) { 375 debug`onCreditCardSave ${aCreditCard}`; 376 377 lazy.EventDispatcher.instance.sendRequest({ 378 type: "GeckoView:Autocomplete:Save:CreditCard", 379 creditCard: aCreditCard, 380 }); 381 }, 382 383 /** 384 * Delegates address entry saving to the attached LoginStorage GeckoView delegate. 385 * Call this when a new or modified address entry has been submitted. 386 * 387 * @param aAddress The {Address} to be saved. 388 */ 389 onAddressSave(aAddress) { 390 debug`onAddressSave ${aAddress}`; 391 392 lazy.EventDispatcher.instance.sendRequest({ 393 type: "GeckoView:Autocomplete:Save:Address", 394 address: aAddress, 395 }); 396 }, 397 398 /** 399 * Delegates login entry saving to the attached LoginStorage GeckoView delegate. 400 * Call this when a new login entry or a new password for an existing login 401 * entry has been submitted. 402 * 403 * @param aLogin The {LoginEntry} to be saved. 404 */ 405 onLoginSave(aLogin) { 406 debug`onLoginSave ${aLogin}`; 407 408 lazy.EventDispatcher.instance.sendRequest({ 409 type: "GeckoView:Autocomplete:Save:Login", 410 login: aLogin, 411 }); 412 }, 413 414 /** 415 * Delegates login entry password usage to the attached LoginStorage GeckoView 416 * delegate. 417 * Call this when the password of an existing login entry, as returned by 418 * fetchLogins, has been used for autofill. 419 * 420 * @param aLogin The {LoginEntry} whose password was used. 421 */ 422 onLoginPasswordUsed(aLogin) { 423 debug`onLoginUsed ${aLogin}`; 424 425 lazy.EventDispatcher.instance.sendRequest({ 426 type: "GeckoView:Autocomplete:Used:Login", 427 usedFields: UsedField.PASSWORD, 428 login: aLogin, 429 }); 430 }, 431 432 _numActiveSelections: 0, 433 434 /** 435 * Delegates login entry selection. 436 * Call this when there are multiple login entry option for a form to delegate 437 * the selection. 438 * 439 * @param aBrowser The browser instance the triggered the selection. 440 * @param aOptions The list of {SelectOption} depicting viable options. 441 */ 442 onLoginSelect(aBrowser, aOptions) { 443 debug`onLoginSelect ${aOptions}`; 444 445 return new Promise((resolve, reject) => { 446 if (!aBrowser || !aOptions) { 447 debug`onLoginSelect Rejecting - no browser or options provided`; 448 reject(); 449 return; 450 } 451 452 const prompt = new lazy.GeckoViewPrompter(aBrowser.ownerGlobal); 453 prompt.asyncShowPrompt( 454 { 455 type: "Autocomplete:Select:Login", 456 options: aOptions, 457 }, 458 result => { 459 if (!result || !result.selection) { 460 reject(); 461 return; 462 } 463 464 const option = new SelectOption({ 465 value: LoginEntry.parse(result.selection.value), 466 hint: result.selection.hint, 467 }); 468 resolve(option); 469 } 470 ); 471 this._prompt = prompt; 472 }); 473 }, 474 475 /** 476 * Delegates credit card entry selection. 477 * Call this when there are multiple credit card entry option for a form to delegate 478 * the selection. 479 * 480 * @param aBrowser The browser instance the triggered the selection. 481 * @param aOptions The list of {SelectOption} depicting viable options. 482 */ 483 onCreditCardSelect(aBrowser, aOptions) { 484 debug`onCreditCardSelect ${aOptions}`; 485 486 return new Promise((resolve, reject) => { 487 if (!aBrowser || !aOptions) { 488 debug`onCreditCardSelect Rejecting - no browser or options provided`; 489 reject(); 490 return; 491 } 492 493 const prompt = new lazy.GeckoViewPrompter(aBrowser.ownerGlobal); 494 prompt.asyncShowPrompt( 495 { 496 type: "Autocomplete:Select:CreditCard", 497 options: aOptions, 498 }, 499 result => { 500 if (!result || !result.selection) { 501 reject(); 502 return; 503 } 504 505 const option = new SelectOption({ 506 value: CreditCard.parse(result.selection.value), 507 hint: result.selection.hint, 508 }); 509 resolve(option); 510 } 511 ); 512 this._prompt = prompt; 513 }); 514 }, 515 516 /** 517 * Delegates address entry selection. 518 * Call this when there are multiple address entry option for a form to delegate 519 * the selection. 520 * 521 * @param aBrowser The browser instance the triggered the selection. 522 * @param aOptions The list of {SelectOption} depicting viable options. 523 */ 524 onAddressSelect(aBrowser, aOptions) { 525 debug`onAddressSelect ${aOptions}`; 526 527 return new Promise((resolve, reject) => { 528 if (!aBrowser || !aOptions) { 529 debug`onAddressSelect Rejecting - no browser or options provided`; 530 reject(); 531 return; 532 } 533 534 const prompt = new lazy.GeckoViewPrompter(aBrowser.ownerGlobal); 535 prompt.asyncShowPrompt( 536 { 537 type: "Autocomplete:Select:Address", 538 options: aOptions, 539 }, 540 result => { 541 if (!result || !result.selection) { 542 reject(); 543 return; 544 } 545 546 const option = new SelectOption({ 547 value: Address.parse(result.selection.value), 548 hint: result.selection.hint, 549 }); 550 resolve(option); 551 } 552 ); 553 this._prompt = prompt; 554 }); 555 }, 556 557 async delegateSelection({ 558 browsingContext, 559 options, 560 inputElementIdentifier, 561 formOrigin, 562 }) { 563 debug`delegateSelection ${options}`; 564 565 if (!options.length) { 566 return; 567 } 568 569 let insecureHint = SelectOption.Hint.NONE; 570 let loginStyle = null; 571 572 // TODO: Replace this string with more robust mechanics. 573 let selectionType = null; 574 const selectOptions = []; 575 576 for (const option of options) { 577 switch (option.style) { 578 case "insecureWarning": { 579 // We depend on the insecure warning to be the first option. 580 insecureHint = SelectOption.Hint.INSECURE_FORM; 581 break; 582 } 583 case "generatedPassword": { 584 selectionType = "login"; 585 const comment = JSON.parse(option.comment); 586 selectOptions.push( 587 new SelectOption({ 588 value: new LoginEntry({ 589 password: comment.generatedPassword, 590 }), 591 hint: SelectOption.Hint.GENERATED | insecureHint, 592 }) 593 ); 594 break; 595 } 596 case "login": 597 // Fallthrough. 598 case "loginWithOrigin": { 599 selectionType = "login"; 600 loginStyle = option.style; 601 const comment = JSON.parse(option.comment); 602 603 let hint = SelectOption.Hint.NONE | insecureHint; 604 if (comment.isDuplicateUsername) { 605 hint |= SelectOption.Hint.DUPLICATE_USERNAME; 606 } 607 if (comment.isOriginMatched) { 608 hint |= SelectOption.Hint.MATCHING_ORIGIN; 609 } 610 611 selectOptions.push( 612 new SelectOption({ 613 value: LoginEntry.parse(comment.fillMessageData), 614 hint, 615 }) 616 ); 617 break; 618 } 619 case "autofill": { 620 const { fillMessageData } = JSON.parse(option.comment); 621 const profile = fillMessageData.profile; 622 debug`delegateSelection - autofill profile ${profile}`; 623 const creditCard = CreditCard.fromGecko(profile); 624 const address = Address.fromGecko(profile); 625 if (creditCard.isValid()) { 626 selectionType = "creditCard"; 627 selectOptions.push( 628 new SelectOption({ 629 value: creditCard, 630 hint: insecureHint, 631 }) 632 ); 633 } else if (address.isValid()) { 634 selectionType = "address"; 635 selectOptions.push( 636 new SelectOption({ 637 value: address, 638 hint: insecureHint, 639 }) 640 ); 641 } 642 break; 643 } 644 default: 645 debug`delegateSelection - ignoring unknown option style ${option.style}`; 646 } 647 } 648 649 if (selectOptions.length < 1) { 650 debug`Abort delegateSelection - no valid options provided`; 651 return; 652 } 653 654 if (this._numActiveSelections > 0) { 655 debug`Abort delegateSelection - there is already one delegation active`; 656 return; 657 } 658 659 ++this._numActiveSelections; 660 661 let selectedOption = null; 662 const browser = browsingContext.top.embedderElement; 663 if (selectionType === "login") { 664 selectedOption = await this.onLoginSelect(browser, selectOptions).catch( 665 _ => { 666 debug`No GV delegate attached`; 667 } 668 ); 669 } else if (selectionType === "creditCard") { 670 selectedOption = await this.onCreditCardSelect( 671 browser, 672 selectOptions 673 ).catch(_ => { 674 debug`No GV delegate attached`; 675 }); 676 } else if (selectionType === "address") { 677 selectedOption = await this.onAddressSelect(browser, selectOptions).catch( 678 _ => { 679 debug`No GV delegate attached`; 680 } 681 ); 682 } 683 684 // prompt is closed now. 685 this._prompt = null; 686 687 --this._numActiveSelections; 688 689 debug`delegateSelection selected option: ${selectedOption}`; 690 691 if (selectionType === "login") { 692 const selectedLogin = selectedOption?.value?.toLoginInfo(); 693 694 if (!selectedLogin) { 695 debug`Abort delegateSelection - no login entry selected`; 696 return; 697 } 698 699 debug`delegateSelection - filling form`; 700 701 if (selectedOption.hint & SelectOption.Hint.GENERATED) { 702 this.onLoginSave(selectedLogin); 703 } 704 705 const actor = 706 browsingContext.currentWindowGlobal.getActor("LoginManager"); 707 708 await actor.fillForm({ 709 browser, 710 inputElementIdentifier, 711 loginFormOrigin: formOrigin, 712 login: selectedLogin, 713 style: 714 selectedOption.hint & SelectOption.Hint.GENERATED 715 ? "generatedPassword" 716 : loginStyle, 717 }); 718 } else if (selectionType === "creditCard") { 719 const actor = 720 browsingContext.currentWindowGlobal.getActor("FormAutofill"); 721 const elementId = JSON.stringify(inputElementIdentifier); 722 const selectedCreditCard = selectedOption?.value?.toGecko(); 723 724 actor.autofillFields(elementId, selectedCreditCard); 725 } else if (selectionType === "address") { 726 const actor = 727 browsingContext.currentWindowGlobal.getActor("FormAutofill"); 728 const elementId = JSON.stringify(inputElementIdentifier); 729 const selectedAddress = selectedOption?.value?.toGecko(); 730 731 actor.autofillFields(elementId, selectedAddress); 732 } 733 734 debug`delegateSelection - form filled`; 735 }, 736 737 delegateDismiss() { 738 debug`delegateDismiss`; 739 740 this._prompt?.dismiss(); 741 }, 742 }; 743 744 const { debug } = GeckoViewUtils.initLogging("GeckoViewAutocomplete");