WebProtocolHandlerRegistrar.sys.mjs (24114B)
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2 /* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 7 8 const STRING_BUNDLE_URI = "chrome://browser/locale/feeds/subscribe.properties"; 9 10 export function WebProtocolHandlerRegistrar() {} 11 12 const lazy = {}; 13 14 XPCOMUtils.defineLazyServiceGetters(lazy, { 15 ExternalProtocolService: [ 16 "@mozilla.org/uriloader/external-protocol-service;1", 17 Ci.nsIExternalProtocolService, 18 ], 19 }); 20 21 ChromeUtils.defineESModuleGetters(lazy, { 22 NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", 23 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 24 }); 25 26 ChromeUtils.defineLazyGetter(lazy, "log", () => { 27 let { ConsoleAPI } = ChromeUtils.importESModule( 28 "resource://gre/modules/Console.sys.mjs" 29 ); 30 let consoleOptions = { 31 // tip: set maxLogLevel to "debug" and use lazy.log.debug() to create 32 // detailed messages during development. See LOG_LEVELS in Console.sys.mjs 33 // for details. 34 maxLogLevel: "warning", 35 maxLogLevelPref: "browser.protocolhandler.loglevel", 36 prefix: "WebProtocolHandlerRegistrar.sys.mjs", 37 }; 38 return new ConsoleAPI(consoleOptions); 39 }); 40 41 WebProtocolHandlerRegistrar.prototype = { 42 get stringBundle() { 43 let sb = Services.strings.createBundle(STRING_BUNDLE_URI); 44 delete WebProtocolHandlerRegistrar.prototype.stringBundle; 45 return (WebProtocolHandlerRegistrar.prototype.stringBundle = sb); 46 }, 47 48 _getFormattedString(key, params) { 49 return this.stringBundle.formatStringFromName(key, params); 50 }, 51 52 _getString(key) { 53 return this.stringBundle.GetStringFromName(key); 54 }, 55 56 /* Because we want to iterate over the known webmailers in the observe method 57 * and with each site visited, we want to check as fast as possible if the 58 * current site is already registered as a mailto handler. Using the sites 59 * domain name as a key ensures that we can use Map.has(...) later to find 60 * it. 61 */ 62 _addedObservers: 0, 63 _knownWebmailerCache: new Map(), 64 _ensureWebmailerCache() { 65 this._knownWebmailerCache = new Map(); 66 67 const handler = 68 lazy.ExternalProtocolService.getProtocolHandlerInfo("mailto"); 69 70 for (const h of handler.possibleApplicationHandlers.enumerate()) { 71 // Services.io.newURI could fail for broken handlers in which case we 72 // simply leave them out, but write a debug message (just in case) 73 try { 74 if (h instanceof Ci.nsIWebHandlerApp && h.uriTemplate) { 75 const mailerUri = Services.io.newURI(h.uriTemplate); 76 if (mailerUri.scheme == "https") { 77 this._knownWebmailerCache.set( 78 Services.io.newURI(h.uriTemplate).host, 79 { 80 uriPath: Services.io.newURI(h.uriTemplate).resolve("."), 81 uriTemplate: Services.io.newURI(h.uriTemplate), 82 name: h.name, 83 } 84 ); 85 } 86 } 87 } catch (e) { 88 lazy.log.debug(`Could not add ${h.uriTemplate} to cache: ${e.message}`); 89 } 90 } 91 }, 92 93 /** 94 * This function can be called multiple times and (re-)initializes the cache 95 * if the feature is toggled on. If called with the feature off it will also 96 * unregister the observers. 97 * 98 * @param {boolean} firstInit 99 */ 100 init(firstInit = false) { 101 if (firstInit) { 102 lazy.NimbusFeatures.mailto.onUpdate(() => 103 // make firstInit explicitly false to avoid multiple registrations. 104 this.init(false) 105 ); 106 } 107 108 const observers = ["mailto::onLocationChange", "mailto::onClearCache"]; 109 if ( 110 lazy.NimbusFeatures.mailto.getVariable("dualPrompt") && 111 lazy.NimbusFeatures.mailto.getVariable("dualPrompt.onLocationChange") 112 ) { 113 this._ensureWebmailerCache(); 114 // Make sure, that our local observers are never registered twice: 115 if (0 == this._addedObservers) { 116 observers.forEach(o => { 117 this._addedObservers++; 118 Services.obs.addObserver(this, o); 119 }); 120 lazy.log.debug(`mailto observers activated: [${observers}]`); 121 } 122 } else { 123 // With `dualPrompt` and `dualPrompt.onLocationChange` toggled on we get 124 // up to two notifications when we turn the feature off again, but we 125 // only want to unregister the observers once. 126 // 127 // Using `hasObservers` would allow us to loop over all observers as long 128 // as there are more, but hasObservers is not implemented hence why we 129 // use `enumerateObservers` here to create the loop and `hasMoreElements` 130 // to return true or false as `hasObservers` would if it existed. 131 observers.forEach(o => { 132 if ( 133 0 < this._addedObservers && 134 Services.obs.enumerateObservers(o).hasMoreElements() 135 ) { 136 Services.obs.removeObserver(this, o); 137 this._addedObservers--; 138 lazy.log.debug(`mailto observer "${o}" deactivated.`); 139 } 140 }); 141 } 142 }, 143 144 async observe(aBrowser, aTopic) { 145 try { 146 switch (aTopic) { 147 case "mailto::onLocationChange": { 148 // registerProtocolHandler only works for https 149 const uri = aBrowser.currentURI; 150 if (!uri?.schemeIs("https")) { 151 return; 152 } 153 154 const host = uri.host; 155 if (this._knownWebmailerCache.has(host)) { 156 // second: search the cache for an entry which starts with the path 157 // of the current uri. If it exists we identified the current page as 158 // webmailer (again). 159 const value = this._knownWebmailerCache.get(host); 160 this._askUserToSetMailtoHandler( 161 aBrowser, 162 "mailto", 163 value.uriTemplate, 164 value.name 165 ); 166 } 167 break; // the switch(topic) statement 168 } 169 case "mailto::onClearCache": 170 // clear the cache for now. We could try to dynamically update the 171 // cache, which is easy if a webmailer is added to the settings, but 172 // becomes more complicated when webmailers are removed, because then 173 // the store gets rewritten and we would require an event to deal with 174 // that as well. So instead we recreate it entirely. 175 this._ensureWebmailerCache(); 176 break; 177 default: 178 lazy.log.debug(`observe reached with unknown topic: ${aTopic}`); 179 } 180 } catch (e) { 181 lazy.log.debug(`Problem in observer: ${e}`); 182 } 183 }, 184 185 /** 186 * See nsIWebProtocolHandlerRegistrar 187 */ 188 removeProtocolHandler(aProtocol, aURITemplate) { 189 let handlerInfo = 190 lazy.ExternalProtocolService.getProtocolHandlerInfo(aProtocol); 191 let handlers = handlerInfo.possibleApplicationHandlers; 192 for (let i = 0; i < handlers.length; i++) { 193 try { 194 // We only want to test web handlers 195 let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp); 196 if (handler.uriTemplate == aURITemplate) { 197 handlers.removeElementAt(i); 198 let hs = Cc["@mozilla.org/uriloader/handler-service;1"].getService( 199 Ci.nsIHandlerService 200 ); 201 hs.store(handlerInfo); 202 return; 203 } 204 } catch (e) { 205 /* it wasn't a web handler */ 206 } 207 } 208 }, 209 210 /** 211 * Determines if a web handler is already registered. 212 * 213 * @param {string} aProtocol 214 * The scheme of the web handler we are checking for. 215 * @param {string} aURITemplate 216 * The URI template that the handler uses to handle the protocol. 217 * @returns {boolean} true if it is already registered, false otherwise. 218 */ 219 _protocolHandlerRegistered(aProtocol, aURITemplate) { 220 let handlerInfo = 221 lazy.ExternalProtocolService.getProtocolHandlerInfo(aProtocol); 222 let handlers = handlerInfo.possibleApplicationHandlers; 223 for (let handler of handlers.enumerate()) { 224 // We only want to test web handlers 225 if ( 226 handler instanceof Ci.nsIWebHandlerApp && 227 handler.uriTemplate == aURITemplate 228 ) { 229 return true; 230 } 231 } 232 return false; 233 }, 234 235 /** 236 * Returns true if aURITemplate.spec points to the currently configured 237 * handler for aProtocol links and the OS default is also readily configured. 238 * Returns false if some of it can be made default. 239 * 240 * @param {string} aProtocol 241 * The scheme of the web handler we are checking for. 242 * @param {string} aURITemplate 243 * The URI template that the handler uses to handle the protocol. 244 */ 245 _isProtocolHandlerDefault(aProtocol, aURITemplate) { 246 const handlerInfo = 247 lazy.ExternalProtocolService.getProtocolHandlerInfo(aProtocol); 248 249 if ( 250 handlerInfo.preferredAction == Ci.nsIHandlerInfo.useHelperApp && 251 handlerInfo.preferredApplicationHandler instanceof Ci.nsIWebHandlerApp 252 ) { 253 let webHandlerApp = 254 handlerInfo.preferredApplicationHandler.QueryInterface( 255 Ci.nsIWebHandlerApp 256 ); 257 258 // If we are already configured as default, we cannot set a new default 259 // and if the current site is already registered as default webmailer we 260 // are fully set up as the default app for webmail. 261 if ( 262 !this._canSetOSDefault(aProtocol) && 263 webHandlerApp.uriTemplate == aURITemplate.spec 264 ) { 265 return true; 266 } 267 } 268 return false; 269 }, 270 271 /** 272 * Private method to return the installHash, which is important for app 273 * registration on OS level. Without it apps cannot be default/handler apps 274 * under Windows. Because this value is used to check if its possible to reset 275 * the default and to actually set it as well, this function puts the 276 * acquisition of the installHash in one place in the hopes that check and set 277 * conditions will never deviate. 278 * 279 * @returns {string} installHash 280 */ 281 _getInstallHash() { 282 const xreDirProvider = Cc[ 283 "@mozilla.org/xre/directory-provider;1" 284 ].getService(Ci.nsIXREDirProvider); 285 return xreDirProvider.getInstallHash(); 286 }, 287 288 /** 289 * Private method to check if we are already the default protocolhandler 290 * for `protocol`. 291 * 292 * @param {string} protocol name, e.g. mailto (without ://) 293 * @returns {boolean} 294 */ 295 _isOsDefault(protocol) { 296 let shellService = Cc[ 297 "@mozilla.org/browser/shell-service;1" 298 ].createInstance(Ci.nsIWindowsShellService); 299 300 if (shellService.isDefaultHandlerFor(protocol)) { 301 lazy.log.debug("_isOsDefault returns true."); 302 return true; 303 } 304 305 lazy.log.debug("_isOsDefault returns false."); 306 return false; 307 }, 308 309 /** 310 * Private method to determine if we can set a new OS default for a certain 311 * protocol. 312 * 313 * @param {string} protocol name, e.g. mailto (without ://) 314 * @returns {boolean} 315 */ 316 _canSetOSDefault(protocol) { 317 // an installHash is required for the association with a scheme handler, 318 // also see _setOSDefault() 319 if ("" == this._getInstallHash()) { 320 lazy.log.debug("_canSetOSDefault returns false."); 321 return false; 322 } 323 324 if (this._isOsDefault(protocol)) { 325 return false; 326 } 327 328 return true; 329 }, 330 331 /** 332 * Private method to reset the OS default for a certain protocol/uri scheme. 333 * We basically ignore, that setDefaultExtensionHandlersUserChoice can fail 334 * when the installHash is wrong or cannot be determined. 335 * 336 * @param {string} protocol name, e.g. mailto (without ://) 337 * @returns {boolean} 338 */ 339 _setOSDefault(protocol) { 340 try { 341 let defaultAgent = Cc["@mozilla.org/default-agent;1"].createInstance( 342 Ci.nsIDefaultAgent 343 ); 344 defaultAgent.setDefaultExtensionHandlersUserChoice( 345 this._getInstallHash(), 346 [protocol, "FirefoxURL"] 347 ); 348 return true; 349 } catch (e) { 350 // TODO: why could not we just add the installHash and promote the running 351 // install to be a properly installed one? 352 lazy.log.debug( 353 "Could not set Firefox as default application for " + 354 protocol + 355 ", because: " + 356 e.message 357 ); 358 } 359 return false; 360 }, 361 362 /** 363 * Private method to set the default uri to handle a certain protocol. This 364 * automates in a way what a user can do in settings under applications, 365 * where different 'actions' can be chosen for different 'content types'. 366 * 367 * @param {string} protocol 368 * @param {handler} handler 369 */ 370 _setProtocolHandlerDefault(protocol, handler) { 371 let handlerInfo = 372 lazy.ExternalProtocolService.getProtocolHandlerInfo(protocol); 373 handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp; 374 handlerInfo.preferredApplicationHandler = handler; 375 handlerInfo.alwaysAskBeforeHandling = false; 376 let hs = Cc["@mozilla.org/uriloader/handler-service;1"].getService( 377 Ci.nsIHandlerService 378 ); 379 hs.store(handlerInfo); 380 return handlerInfo; 381 }, 382 383 /** 384 * Private method to add a ProtocolHandler of type nsIWebHandlerApp to the 385 * list of possible handlers for a protocol. 386 * 387 * @param {string} protocol - e.g. 'mailto', so again without :// 388 * @param {string} name - the protocol associated 'Action' 389 * @param {string} uri - the uri (compare 'use other...' in the preferences) 390 * @returns {handler} handler - either the existing one or a newly created 391 */ 392 _addWebProtocolHandler(protocol, name, uri) { 393 let phi = lazy.ExternalProtocolService.getProtocolHandlerInfo(protocol); 394 // not adding duplicates and bail out with the existing entry 395 for (let h of phi.possibleApplicationHandlers.enumerate()) { 396 if (h instanceof Ci.nsIWebHandlerApp && h.uriTemplate === uri) { 397 return h; 398 } 399 } 400 401 let handler = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance( 402 Ci.nsIWebHandlerApp 403 ); 404 handler.name = name; 405 handler.uriTemplate = uri; 406 407 let handlerInfo = 408 lazy.ExternalProtocolService.getProtocolHandlerInfo(protocol); 409 handlerInfo.possibleApplicationHandlers.appendElement(handler); 410 411 let hs = Cc["@mozilla.org/uriloader/handler-service;1"].getService( 412 Ci.nsIHandlerService 413 ); 414 hs.store(handlerInfo); 415 416 return handler; 417 }, 418 419 /** 420 * See nsIWebProtocolHandlerRegistrar 421 */ 422 registerProtocolHandler( 423 aProtocol, 424 aURI, 425 aTitle, 426 aDocumentURI, 427 aBrowserOrWindow 428 ) { 429 // first mitigation: check if the API call comes from another domain 430 aProtocol = (aProtocol || "").toLowerCase(); 431 if (!aURI || !aDocumentURI) { 432 return; 433 } 434 435 let browser = aBrowserOrWindow; // This is the e10s case. 436 if (aBrowserOrWindow instanceof Ci.nsIDOMWindow) { 437 // In the non-e10s case, grab the browser off the same-process window. 438 let rootDocShell = aBrowserOrWindow.docShell.sameTypeRootTreeItem; 439 browser = rootDocShell.QueryInterface(Ci.nsIDocShell).chromeEventHandler; 440 } 441 442 let browserWindow = browser.ownerGlobal; 443 try { 444 browserWindow.navigator.checkProtocolHandlerAllowed( 445 aProtocol, 446 aURI, 447 aDocumentURI 448 ); 449 } catch (ex) { 450 // We should have already shown the user an error. 451 return; 452 } 453 if (lazy.NimbusFeatures.mailto.getVariable("dualPrompt")) { 454 if ("mailto" === aProtocol) { 455 lazy.NimbusFeatures.mailto.recordExposureEvent(); 456 this._askUserToSetMailtoHandler(browser, aProtocol, aURI, aTitle); 457 return; 458 } 459 } 460 461 // If the protocol handler is already registered, just return early. 462 if (this._protocolHandlerRegistered(aProtocol, aURI.spec)) { 463 return; 464 } 465 466 // Now Ask the user and provide the proper callback 467 let message = this._getFormattedString("addProtocolHandlerMessage", [ 468 aURI.host, 469 aProtocol, 470 ]); 471 472 let notificationIcon = aURI.prePath + "/favicon.ico"; 473 let notificationValue = "Protocol Registration: " + aProtocol; 474 let addButton = { 475 label: this._getString("addProtocolHandlerAddButton"), 476 accessKey: this._getString("addProtocolHandlerAddButtonAccesskey"), 477 protocolInfo: { protocol: aProtocol, uri: aURI.spec, name: aTitle }, 478 primary: true, 479 480 callback(aNotification, aButtonInfo) { 481 let protocol = aButtonInfo.protocolInfo.protocol; 482 let name = aButtonInfo.protocolInfo.name; 483 484 let handler = Cc[ 485 "@mozilla.org/uriloader/web-handler-app;1" 486 ].createInstance(Ci.nsIWebHandlerApp); 487 handler.name = name; 488 handler.uriTemplate = aButtonInfo.protocolInfo.uri; 489 490 let handlerInfo = 491 lazy.ExternalProtocolService.getProtocolHandlerInfo(protocol); 492 handlerInfo.possibleApplicationHandlers.appendElement(handler); 493 494 // Since the user has agreed to add a new handler, chances are good 495 // that the next time they see a handler of this type, they're going 496 // to want to use it. Reset the handlerInfo to ask before the next 497 // use. 498 handlerInfo.alwaysAskBeforeHandling = true; 499 500 let hs = Cc["@mozilla.org/uriloader/handler-service;1"].getService( 501 Ci.nsIHandlerService 502 ); 503 hs.store(handlerInfo); 504 }, 505 }; 506 507 let notificationBox = browser.getTabBrowser().getNotificationBox(browser); 508 509 // check if the notification box is already shown 510 if (notificationBox.getNotificationWithValue(notificationValue)) { 511 return; 512 } 513 514 notificationBox.appendNotification( 515 notificationValue, 516 { 517 label: message, 518 image: notificationIcon, 519 priority: notificationBox.PRIORITY_INFO_LOW, 520 }, 521 [addButton] 522 ); 523 }, 524 525 /** 526 * Special implementation for mailto: A prompt (notificationbox.js) is only 527 * shown if there is a realistic chance that we can really set the OS default, 528 * e.g. if we have been properly installed and the current page is not already 529 * the default and we have not asked users too often the same question. 530 * 531 * @param {string} browser 532 * @param {string} aProtocol 533 * @param {nsIURI} aURI 534 * @param {string} aTitle 535 */ 536 async _askUserToSetMailtoHandler(browser, aProtocol, aURI, aTitle) { 537 let notificationId = "OS Protocol Registration: " + aProtocol; 538 539 // guard: we do not want to reconfigure settings in private browsing mode 540 if (lazy.PrivateBrowsingUtils.isWindowPrivate(browser.ownerGlobal)) { 541 lazy.log.debug("prompt not shown, because this is a private window."); 542 return; 543 } 544 545 // guard: check if everything has been configured to use the current site 546 // as default webmailer and bail out if so. 547 if (this._isProtocolHandlerDefault(aProtocol, aURI)) { 548 lazy.log.debug( 549 `prompt not shown, because ${aTitle} is already configured to` + 550 ` handle ${aProtocol}-links under ${aURI.spec}.` 551 ); 552 return; 553 } 554 555 // guard: bail out if this site has been dismissed before (either by 556 // clicking the 'x' button or the 'not now' button. 557 let principal = browser.getTabBrowser().contentPrincipal; 558 if ( 559 Ci.nsIPermissionManager.DENY_ACTION == 560 Services.perms.testExactPermissionFromPrincipal( 561 principal, 562 "mailto-infobar-dismissed" 563 ) 564 ) { 565 let expiry = 566 Services.perms.getPermissionObject( 567 principal, 568 "mailto-infobar-dismissed", 569 true 570 ).expireTime - Date.now(); 571 572 lazy.log.debug( 573 `prompt not shown, because it is still dismissed for` + 574 ` ${principal.host} and will be shown in` + 575 ` ${(expiry / 1000).toFixed()} seconds again.` 576 ); 577 return; 578 } 579 580 // Now show the prompt if there is not already one... 581 let osDefaultNotificationBox = browser 582 .getTabBrowser() 583 .getNotificationBox(browser); 584 585 if (!osDefaultNotificationBox.getNotificationWithValue(notificationId)) { 586 let win = browser.ownerGlobal; 587 win.MozXULElement.insertFTLIfNeeded("browser/webProtocolHandler.ftl"); 588 589 let notification = await osDefaultNotificationBox.appendNotification( 590 notificationId, 591 { 592 label: { 593 "l10n-id": "protocolhandler-mailto-handler-set", 594 "l10n-args": { url: aURI.host }, 595 }, 596 priority: osDefaultNotificationBox.PRIORITY_INFO_LOW, 597 eventCallback: eventType => { 598 if (eventType === "dismissed") { 599 // after a click on 'X' save a timestamp after which we can show 600 // the prompt again 601 Services.perms.addFromPrincipal( 602 principal, 603 "mailto-infobar-dismissed", 604 Ci.nsIPermissionManager.DENY_ACTION, 605 Ci.nsIPermissionManager.EXPIRE_TIME, 606 lazy.NimbusFeatures.mailto.getVariable( 607 "dualPrompt.dismissXClickMinutes" 608 ) * 609 60 * 610 1000 + 611 Date.now() 612 ); 613 Glean.protocolhandlerMailto.promptClicked.dismiss_os_default.add(); 614 } 615 }, 616 }, 617 [ 618 { 619 "l10n-id": "protocolhandler-mailto-os-handler-yes-button", 620 primary: true, 621 callback: newitem => { 622 let currentHandler = this._addWebProtocolHandler( 623 aProtocol, 624 aTitle, 625 aURI.spec 626 ); 627 this._setProtocolHandlerDefault(aProtocol, currentHandler); 628 Glean.protocolhandlerMailto.promptClicked.set_local_default.add(); 629 630 if (this._canSetOSDefault(aProtocol)) { 631 if (this._setOSDefault(aProtocol)) { 632 Glean.protocolhandlerMailto.promptClicked.set_os_default.add(); 633 newitem.messageL10nId = 634 "protocolhandler-mailto-handler-confirm"; 635 newitem.removeChild(newitem.buttonContainer); 636 newitem.setAttribute("type", "success"); // from moz-message-bar.css 637 newitem.eventCallback = null; // disable show only once per day for success 638 return true; // `true` does not hide the bar 639 } 640 641 // if anything goes wrong with setting the OS default, we want 642 // to be informed so that we can fix it. 643 Glean.protocolhandlerMailto.promptClicked.set_os_default_error.add(); 644 return false; 645 } 646 647 // if the installation does not have an install hash, we cannot 648 // set the OS default, but mailto links from within the browser 649 // should still work. 650 Glean.protocolhandlerMailto.promptClicked.set_os_default_impossible.add(); 651 return false; 652 }, 653 }, 654 { 655 "l10n-id": "protocolhandler-mailto-os-handler-no-button", 656 callback: () => { 657 // after a click on 'Not Now' save a timestamp after which we can 658 // show the prompt again 659 Services.perms.addFromPrincipal( 660 principal, 661 "mailto-infobar-dismissed", 662 Ci.nsIPermissionManager.DENY_ACTION, 663 Ci.nsIPermissionManager.EXPIRE_TIME, 664 lazy.NimbusFeatures.mailto.getVariable( 665 "dualPrompt.dismissNotNowMinutes" 666 ) * 667 60 * 668 1000 + 669 Date.now() 670 ); 671 Glean.protocolhandlerMailto.promptClicked.dismiss_os_default.add(); 672 return false; 673 }, 674 }, 675 ] 676 ); 677 678 Glean.protocolhandlerMailto.handlerPromptShown.os_default.add(); 679 // remove the icon from the infobar, which is automatically assigned 680 // after its priority, because the priority is also an indicator which 681 // type of bar it is, e.g. a warning or error: 682 notification.setAttribute("type", "system"); 683 } 684 }, 685 686 /** 687 * See nsISupports 688 */ 689 QueryInterface: ChromeUtils.generateQI(["nsIWebProtocolHandlerRegistrar"]), 690 };