dom/browser-element/BrowserElementPromptService.jsm

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rw-r--r--

Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2

     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 /* vim: set ft=javascript : */
     6 "use strict";
     8 let Cu = Components.utils;
     9 let Ci = Components.interfaces;
    10 let Cc = Components.classes;
    11 let Cr = Components.results;
    12 let Cm = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
    14 this.EXPORTED_SYMBOLS = ["BrowserElementPromptService"];
    16 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    17 Cu.import("resource://gre/modules/Services.jsm");
    19 const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
    20 const BROWSER_FRAMES_ENABLED_PREF = "dom.mozBrowserFramesEnabled";
    22 function debug(msg) {
    23   //dump("BrowserElementPromptService - " + msg + "\n");
    24 }
    26 function BrowserElementPrompt(win, browserElementChild) {
    27   this._win = win;
    28   this._browserElementChild = browserElementChild;
    29 }
    31 BrowserElementPrompt.prototype = {
    32   QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]),
    34   alert: function(title, text) {
    35     this._browserElementChild.showModalPrompt(
    36       this._win, {promptType: "alert", title: title, message: text, returnValue: undefined});
    37   },
    39   alertCheck: function(title, text, checkMsg, checkState) {
    40     // Treat this like a normal alert() call, ignoring the checkState.  The
    41     // front-end can do its own suppression of the alert() if it wants.
    42     this.alert(title, text);
    43   },
    45   confirm: function(title, text) {
    46     return this._browserElementChild.showModalPrompt(
    47       this._win, {promptType: "confirm", title: title, message: text, returnValue: undefined});
    48   },
    50   confirmCheck: function(title, text, checkMsg, checkState) {
    51     return this.confirm(title, text);
    52   },
    54   // Each button is described by an object with the following schema
    55   // {
    56   //   string messageType,  // 'builtin' or 'custom'
    57   //   string message, // 'ok', 'cancel', 'yes', 'no', 'save', 'dontsave', 
    58   //                   // 'revert' or a string from caller if messageType was 'custom'.
    59   // }
    60   //
    61   // Expected result from embedder:
    62   // {
    63   //   int button, // Index of the button that user pressed.
    64   //   boolean checked, // True if the check box is checked.
    65   // }
    66   confirmEx: function(title, text, buttonFlags, button0Title, button1Title,
    67                       button2Title, checkMsg, checkState) {
    68     let buttonProperties = this._buildConfirmExButtonProperties(buttonFlags,
    69                                                                 button0Title,
    70                                                                 button1Title,
    71                                                                 button2Title);
    72     let defaultReturnValue = { selectedButton: buttonProperties.defaultButton };
    73     if (checkMsg) {
    74       defaultReturnValue.checked = checkState.value;
    75     }
    76     let ret = this._browserElementChild.showModalPrompt(
    77       this._win,
    78       {
    79         promptType: "custom-prompt",
    80         title: title,
    81         message: text,
    82         defaultButton: buttonProperties.defaultButton,
    83         buttons: buttonProperties.buttons,
    84         showCheckbox: !!checkMsg,
    85         checkboxMessage: checkMsg,
    86         checkboxCheckedByDefault: !!checkState.value,
    87         returnValue: defaultReturnValue
    88       }
    89     );
    90     if (checkMsg) {
    91       checkState.value = ret.checked;
    92     }
    93     return buttonProperties.indexToButtonNumberMap[ret.selectedButton];
    94   },
    96   prompt: function(title, text, value, checkMsg, checkState) {
    97     let rv = this._browserElementChild.showModalPrompt(
    98       this._win,
    99       { promptType: "prompt",
   100         title: title,
   101         message: text,
   102         initialValue: value.value,
   103         returnValue: null });
   105     value.value = rv;
   107     // nsIPrompt::Prompt returns true if the user pressed "OK" at the prompt,
   108     // and false if the user pressed "Cancel".
   109     //
   110     // BrowserElementChild returns null for "Cancel" and returns the string the
   111     // user entered otherwise.
   112     return rv !== null;
   113   },
   115   promptUsernameAndPassword: function(title, text, username, password, checkMsg, checkState) {
   116     throw Cr.NS_ERROR_NOT_IMPLEMENTED;
   117   },
   119   promptPassword: function(title, text, password, checkMsg, checkState) {
   120     throw Cr.NS_ERROR_NOT_IMPLEMENTED;
   121   },
   123   select: function(title, text, aCount, aSelectList, aOutSelection) {
   124     throw Cr.NS_ERROR_NOT_IMPLEMENTED;
   125   },
   127   _buildConfirmExButtonProperties: function(buttonFlags, button0Title,
   128                                             button1Title, button2Title) {
   129     let r = {
   130       defaultButton: -1,
   131       buttons: [],
   132       // This map is for translating array index to the button number that
   133       // is recognized by Gecko. This shouldn't be exposed to embedder.
   134       indexToButtonNumberMap: []
   135     };
   137     let defaultButton = 0;  // Default to Button 0.
   138     if (buttonFlags & Ci.nsIPrompt.BUTTON_POS_1_DEFAULT) {
   139       defaultButton = 1;
   140     } else if (buttonFlags & Ci.nsIPrompt.BUTTON_POS_2_DEFAULT) {
   141       defaultButton = 2;
   142     }
   144     // Properties of each button.
   145     let buttonPositions = [
   146       Ci.nsIPrompt.BUTTON_POS_0,
   147       Ci.nsIPrompt.BUTTON_POS_1,
   148       Ci.nsIPrompt.BUTTON_POS_2
   149     ];
   151     function buildButton(buttonTitle, buttonNumber) {
   152       let ret = {};
   153       let buttonPosition = buttonPositions[buttonNumber];
   154       let mask = 0xff * buttonPosition;  // 8 bit mask
   155       let titleType = (buttonFlags & mask) / buttonPosition;
   157       ret.messageType = 'builtin';
   158       switch(titleType) {
   159       case Ci.nsIPrompt.BUTTON_TITLE_OK:
   160         ret.message = 'ok';
   161         break;
   162       case Ci.nsIPrompt.BUTTON_TITLE_CANCEL:
   163         ret.message = 'cancel';
   164         break;
   165       case Ci.nsIPrompt.BUTTON_TITLE_YES:
   166         ret.message = 'yes';
   167         break;
   168       case Ci.nsIPrompt.BUTTON_TITLE_NO:
   169         ret.message = 'no';
   170         break;
   171       case Ci.nsIPrompt.BUTTON_TITLE_SAVE:
   172         ret.message = 'save';
   173         break;
   174       case Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE:
   175         ret.message = 'dontsave';
   176         break;
   177       case Ci.nsIPrompt.BUTTON_TITLE_REVERT:
   178         ret.message = 'revert';
   179         break;
   180       case Ci.nsIPrompt.BUTTON_TITLE_IS_STRING:
   181         ret.message = buttonTitle;
   182         ret.messageType = 'custom';
   183         break;
   184       default:
   185         // This button is not shown.
   186         return;
   187       }
   189       // If this is the default button, set r.defaultButton to
   190       // the index of this button in the array. This value is going to be
   191       // exposed to the embedder.
   192       if (defaultButton === buttonNumber) {
   193         r.defaultButton = r.buttons.length;
   194       }
   195       r.buttons.push(ret);
   196       r.indexToButtonNumberMap.push(buttonNumber);
   197     }
   199     buildButton(button0Title, 0);
   200     buildButton(button1Title, 1);
   201     buildButton(button2Title, 2);
   203     // If defaultButton is still -1 here, it means the default button won't
   204     // be shown.
   205     if (r.defaultButton === -1) {
   206       throw new Components.Exception("Default button won't be shown",
   207                                      Cr.NS_ERROR_FAILURE);
   208     }
   210     return r;
   211   },
   212 };
   215 function BrowserElementAuthPrompt() {
   216 }
   218 BrowserElementAuthPrompt.prototype = {
   219   QueryInterface: XPCOMUtils.generateQI([Ci.nsIAuthPrompt2]),
   221   promptAuth: function promptAuth(channel, level, authInfo) {
   222     throw Cr.NS_ERROR_NOT_IMPLEMENTED;
   223   },
   225   asyncPromptAuth: function asyncPromptAuth(channel, callback, context, level, authInfo) {
   226     debug("asyncPromptAuth");
   228     // The cases that we don't support now.
   229     if ((authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) &&
   230         (authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD)) {
   231       throw Cr.NS_ERROR_FAILURE;
   232     }
   234     let frame = this._getFrameFromChannel(channel);
   235     if (!frame) {
   236       debug("Cannot get frame, asyncPromptAuth fail");
   237       throw Cr.NS_ERROR_FAILURE;
   238     }
   240     let browserElementParent =
   241       BrowserElementPromptService.getBrowserElementParentForFrame(frame);
   243     if (!browserElementParent) {
   244       debug("Failed to load browser element parent.");
   245       throw Cr.NS_ERROR_FAILURE;
   246     }
   248     let consumer = {
   249       QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
   250       callback: callback,
   251       context: context,
   252       cancel: function() {
   253         this.callback.onAuthCancelled(this.context, false);
   254         this.callback = null;
   255         this.context = null;
   256       }
   257     };
   259     let [hostname, httpRealm] = this._getAuthTarget(channel, authInfo);
   260     let hashKey = level + "|" + hostname + "|" + httpRealm;
   261     let asyncPrompt = this._asyncPrompts[hashKey];
   262     if (asyncPrompt) {
   263       asyncPrompt.consumers.push(consumer);
   264       return consumer;
   265     }
   267     asyncPrompt = {
   268       consumers: [consumer],
   269       channel: channel,
   270       authInfo: authInfo,
   271       level: level,
   272       inProgress: false,
   273       browserElementParent: browserElementParent
   274     };
   276     this._asyncPrompts[hashKey] = asyncPrompt;
   277     this._doAsyncPrompt();
   278     return consumer;
   279   },
   281   // Utilities for nsIAuthPrompt2 ----------------
   283   _asyncPrompts: {},
   284   _asyncPromptInProgress: new WeakMap(),
   285   _doAsyncPrompt: function() {
   286     // Find the key of a prompt whose browser element parent does not have
   287     // async prompt in progress.
   288     let hashKey = null;
   289     for (let key in this._asyncPrompts) {
   290       let prompt = this._asyncPrompts[key];
   291       if (!this._asyncPromptInProgress.get(prompt.browserElementParent)) {
   292         hashKey = key;
   293         break;
   294       }
   295     }
   297     // Didn't find an available prompt, so just return.
   298     if (!hashKey)
   299       return;
   301     let prompt = this._asyncPrompts[hashKey];
   302     let [hostname, httpRealm] = this._getAuthTarget(prompt.channel,
   303                                                     prompt.authInfo);
   305     this._asyncPromptInProgress.set(prompt.browserElementParent, true);
   306     prompt.inProgress = true;
   308     let self = this;
   309     let callback = function(ok, username, password) {
   310       debug("Async auth callback is called, ok = " +
   311             ok + ", username = " + username);
   313       // Here we got the username and password provided by embedder, or
   314       // ok = false if the prompt was cancelled by embedder.
   315       delete self._asyncPrompts[hashKey];
   316       prompt.inProgress = false;
   317       self._asyncPromptInProgress.delete(prompt.browserElementParent);
   319       // Fill authentication information with username and password provided
   320       // by user.
   321       let flags = prompt.authInfo.flags;
   322       if (username) {
   323         if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
   324           // Domain is separated from username by a backslash
   325           let idx = username.indexOf("\\");
   326           if (idx == -1) {
   327             prompt.authInfo.username = username;
   328           } else {
   329             prompt.authInfo.domain   = username.substring(0, idx);
   330             prompt.authInfo.username = username.substring(idx + 1);
   331           }
   332         } else {
   333           prompt.authInfo.username = username;
   334         }
   335       }
   337       if (password) {
   338         prompt.authInfo.password = password;
   339       }
   341       for each (let consumer in prompt.consumers) {
   342         if (!consumer.callback) {
   343           // Not having a callback means that consumer didn't provide it
   344           // or canceled the notification.
   345           continue;
   346         }
   348         try {
   349           if (ok) {
   350             debug("Ok, calling onAuthAvailable to finish auth");
   351             consumer.callback.onAuthAvailable(consumer.context, prompt.authInfo);
   352           } else {
   353             debug("Cancelled, calling onAuthCancelled to finish auth.");
   354             consumer.callback.onAuthCancelled(consumer.context, true);
   355           }
   356         } catch (e) { /* Throw away exceptions caused by callback */ }
   357       }
   359       // Process the next prompt, if one is pending.
   360       self._doAsyncPrompt();
   361     };
   363     let runnable = {
   364       run: function() {
   365         // Call promptAuth of browserElementParent, to show the prompt.
   366         prompt.browserElementParent.promptAuth(
   367           self._createAuthDetail(prompt.channel, prompt.authInfo),
   368           callback);
   369       }
   370     }
   372     Services.tm.currentThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL);
   373   },
   375   _getFrameFromChannel: function(channel) {
   376     let loadContext = channel.notificationCallbacks.getInterface(Ci.nsILoadContext);
   377     return loadContext.topFrameElement;
   378   },
   380   _createAuthDetail: function(channel, authInfo) {
   381     let [hostname, httpRealm] = this._getAuthTarget(channel, authInfo);
   382     return {
   383       host:             hostname,
   384       realm:            httpRealm,
   385       username:         authInfo.username,
   386       isOnlyPassword:   !!(authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD)
   387     };
   388   },
   390   _getAuthTarget : function (channel, authInfo) {
   391     let hostname = this._getFormattedHostname(channel.URI);
   393     // If a HTTP WWW-Authenticate header specified a realm, that value
   394     // will be available here. If it wasn't set or wasn't HTTP, we'll use
   395     // the formatted hostname instead.
   396     let realm = authInfo.realm;
   397     if (!realm)
   398       realm = hostname;
   400     return [hostname, realm];
   401   },
   403   _getFormattedHostname : function(uri) {
   404     let scheme = uri.scheme;
   405     let hostname = scheme + "://" + uri.host;
   407     // If the URI explicitly specified a port, only include it when
   408     // it's not the default. (We never want "http://foo.com:80")
   409     let port = uri.port;
   410     if (port != -1) {
   411       let handler = Services.io.getProtocolHandler(scheme);
   412       if (port != handler.defaultPort)
   413         hostname += ":" + port;
   414     }
   415     return hostname;
   416   }
   417 };
   420 function AuthPromptWrapper(oldImpl, browserElementImpl) {
   421   this._oldImpl = oldImpl;
   422   this._browserElementImpl = browserElementImpl;
   423 }
   425 AuthPromptWrapper.prototype = {
   426   QueryInterface: XPCOMUtils.generateQI([Ci.nsIAuthPrompt2]),
   427   promptAuth: function(channel, level, authInfo) {
   428     if (this._canGetParentElement(channel)) {
   429       return this._browserElementImpl.promptAuth(channel, level, authInfo);
   430     } else {
   431       return this._oldImpl.promptAuth(channel, level, authInfo);
   432     }
   433   },
   435   asyncPromptAuth: function(channel, callback, context, level, authInfo) {
   436     if (this._canGetParentElement(channel)) {
   437       return this._browserElementImpl.asyncPromptAuth(channel, callback, context, level, authInfo);
   438     } else {
   439       return this._oldImpl.asyncPromptAuth(channel, callback, context, level, authInfo);
   440     }
   441   },
   443   _canGetParentElement: function(channel) {
   444     try {
   445       let frame = channel.notificationCallbacks.getInterface(Ci.nsILoadContext).topFrameElement;
   446       if (!frame)
   447         return false;
   449       if (!BrowserElementPromptService.getBrowserElementParentForFrame(frame))
   450         return false;
   452       return true;
   453     } catch (e) {
   454       return false;
   455     }
   456   }
   457 };
   459 function BrowserElementPromptFactory(toWrap) {
   460   this._wrapped = toWrap;
   461 }
   463 BrowserElementPromptFactory.prototype = {
   464   classID: Components.ID("{24f3d0cf-e417-4b85-9017-c9ecf8bb1299}"),
   465   QueryInterface: XPCOMUtils.generateQI([Ci.nsIPromptFactory]),
   467   _mayUseNativePrompt: function() {
   468     try {
   469       return Services.prefs.getBoolPref("browser.prompt.allowNative");
   470     } catch (e) {
   471       // This properity is default to true.
   472       return true;
   473     }
   474   },
   476   _getNativePromptIfAllowed: function(win, iid, err) {
   477     if (this._mayUseNativePrompt())
   478       return this._wrapped.getPrompt(win, iid);
   479     else {
   480       // Not allowed, throw an exception.
   481       throw err;
   482     }
   483   },
   485   getPrompt: function(win, iid) {
   486     // It is possible for some object to get a prompt without passing
   487     // valid reference of window, like nsNSSComponent. In such case, we
   488     // should just fall back to the native prompt service
   489     if (!win)
   490       return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_INVALID_ARG);
   492     if (iid.number != Ci.nsIPrompt.number &&
   493         iid.number != Ci.nsIAuthPrompt2.number) {
   494       debug("We don't recognize the requested IID (" + iid + ", " +
   495             "allowed IID: " +
   496             "nsIPrompt=" + Ci.nsIPrompt + ", " +
   497             "nsIAuthPrompt2=" + Ci.nsIAuthPrompt2 + ")");
   498       return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_INVALID_ARG);
   499     }
   501     // Try to find a BrowserElementChild for the window.
   502     let browserElementChild =
   503       BrowserElementPromptService.getBrowserElementChildForWindow(win);
   505     if (iid.number === Ci.nsIAuthPrompt2.number) {
   506       debug("Caller requests an instance of nsIAuthPrompt2.");
   508       if (browserElementChild) {
   509         // If we are able to get a BrowserElementChild, it means that
   510         // the auth prompt is for a mozbrowser. Therefore we don't need to
   511         // fall back.
   512         return new BrowserElementAuthPrompt().QueryInterface(iid);
   513       }
   515       // Because nsIAuthPrompt2 is called in parent process. If caller
   516       // wants nsIAuthPrompt2 and we cannot get BrowserElementchild,
   517       // it doesn't mean that we should fallback. It is possible that we can
   518       // get the BrowserElementParent from nsIChannel that passed to
   519       // functions of nsIAuthPrompt2.
   520       if (this._mayUseNativePrompt()) {
   521         return new AuthPromptWrapper(
   522             this._wrapped.getPrompt(win, iid),
   523             new BrowserElementAuthPrompt().QueryInterface(iid))
   524           .QueryInterface(iid);
   525       } else {
   526         // Falling back is not allowed, so we don't need wrap the
   527         // BrowserElementPrompt.
   528         return new BrowserElementAuthPrompt().QueryInterface(iid);
   529       }
   530     }
   532     if (!browserElementChild) {
   533       debug("We can't find a browserElementChild for " +
   534             win + ", " + win.location);
   535       return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_FAILURE);
   536     }
   538     debug("Returning wrapped getPrompt for " + win);
   539     return new BrowserElementPrompt(win, browserElementChild)
   540                                    .QueryInterface(iid);
   541   }
   542 };
   544 this.BrowserElementPromptService = {
   545   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
   546                                          Ci.nsISupportsWeakReference]),
   548   _initialized: false,
   550   _init: function() {
   551     if (this._initialized) {
   552       return;
   553     }
   555     // If the pref is disabled, do nothing except wait for the pref to change.
   556     if (!this._browserFramesPrefEnabled()) {
   557       var prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
   558       prefs.addObserver(BROWSER_FRAMES_ENABLED_PREF, this, /* ownsWeak = */ true);
   559       return;
   560     }
   562     this._initialized = true;
   563     this._browserElementParentMap = new WeakMap();
   565     var os = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
   566     os.addObserver(this, "outer-window-destroyed", /* ownsWeak = */ true);
   568     // Wrap the existing @mozilla.org/prompter;1 implementation.
   569     var contractID = "@mozilla.org/prompter;1";
   570     var oldCID = Cm.contractIDToCID(contractID);
   571     var newCID = BrowserElementPromptFactory.prototype.classID;
   572     var oldFactory = Cm.getClassObject(Cc[contractID], Ci.nsIFactory);
   574     if (oldCID == newCID) {
   575       debug("WARNING: Wrapped prompt factory is already installed!");
   576       return;
   577     }
   579     Cm.unregisterFactory(oldCID, oldFactory);
   581     var oldInstance = oldFactory.createInstance(null, Ci.nsIPromptFactory);
   582     var newInstance = new BrowserElementPromptFactory(oldInstance);
   584     var newFactory = {
   585       createInstance: function(outer, iid) {
   586         if (outer != null) {
   587           throw Cr.NS_ERROR_NO_AGGREGATION;
   588         }
   589         return newInstance.QueryInterface(iid);
   590       }
   591     };
   592     Cm.registerFactory(newCID,
   593                        "BrowserElementPromptService's prompter;1 wrapper",
   594                        contractID, newFactory);
   596     debug("Done installing new prompt factory.");
   597   },
   599   _getOuterWindowID: function(win) {
   600     return win.QueryInterface(Ci.nsIInterfaceRequestor)
   601               .getInterface(Ci.nsIDOMWindowUtils)
   602               .outerWindowID;
   603   },
   605   _browserElementChildMap: {},
   606   mapWindowToBrowserElementChild: function(win, browserElementChild) {
   607     this._browserElementChildMap[this._getOuterWindowID(win)] = browserElementChild;
   608   },
   610   getBrowserElementChildForWindow: function(win) {
   611     // We only have a mapping for <iframe mozbrowser>s, not their inner
   612     // <iframes>, so we look up win.top below.  window.top (when called from
   613     // script) respects <iframe mozbrowser> boundaries.
   614     return this._browserElementChildMap[this._getOuterWindowID(win.top)];
   615   },
   617   mapFrameToBrowserElementParent: function(frame, browserElementParent) {
   618     this._browserElementParentMap.set(frame, browserElementParent);
   619   },
   621   getBrowserElementParentForFrame: function(frame) {
   622     return this._browserElementParentMap.get(frame);
   623   },
   625   _observeOuterWindowDestroyed: function(outerWindowID) {
   626     let id = outerWindowID.QueryInterface(Ci.nsISupportsPRUint64).data;
   627     debug("observeOuterWindowDestroyed " + id);
   628     delete this._browserElementChildMap[outerWindowID.data];
   629   },
   631   _browserFramesPrefEnabled: function() {
   632     var prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
   633     try {
   634       return prefs.getBoolPref(BROWSER_FRAMES_ENABLED_PREF);
   635     }
   636     catch(e) {
   637       return false;
   638     }
   639   },
   641   observe: function(subject, topic, data) {
   642     switch(topic) {
   643     case NS_PREFBRANCH_PREFCHANGE_TOPIC_ID:
   644       if (data == BROWSER_FRAMES_ENABLED_PREF) {
   645         this._init();
   646       }
   647       break;
   648     case "outer-window-destroyed":
   649       this._observeOuterWindowDestroyed(subject);
   650       break;
   651     default:
   652       debug("Observed unexpected topic " + topic);
   653     }
   654   }
   655 };
   657 BrowserElementPromptService._init();

mercurial