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