PageRenderTime 66ms CodeModel.GetById 30ms RepoModel.GetById 0ms app.codeStats 1ms

/toolkit/components/passwordmgr/nsLoginManager.js

http://github.com/zpao/v8monkey
JavaScript | 1433 lines | 764 code | 255 blank | 414 comment | 208 complexity | d6051b2d4886eb0f85bca66486fdf778 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, LGPL-3.0, AGPL-1.0, LGPL-2.1, BSD-3-Clause, GPL-2.0, JSON, Apache-2.0, 0BSD

Large files files are truncated, but you can click here to view the full file

  1. /* ***** BEGIN LICENSE BLOCK *****
  2. * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3. *
  4. * The contents of this file are subject to the Mozilla Public License Version
  5. * 1.1 (the "License"); you may not use this file except in compliance with
  6. * the License. You may obtain a copy of the License at
  7. * http://www.mozilla.org/MPL/
  8. *
  9. * Software distributed under the License is distributed on an "AS IS" basis,
  10. * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11. * for the specific language governing rights and limitations under the
  12. * License.
  13. *
  14. * The Original Code is mozilla.org code.
  15. *
  16. * The Initial Developer of the Original Code is Mozilla Foundation.
  17. * Portions created by the Initial Developer are Copyright (C) 2007
  18. * the Initial Developer. All Rights Reserved.
  19. *
  20. * Contributor(s):
  21. * Justin Dolske <dolske@mozilla.com> (original author)
  22. * Ehsan Akhgari <ehsan.akhgari@gmail.com>
  23. *
  24. * Alternatively, the contents of this file may be used under the terms of
  25. * either the GNU General Public License Version 2 or later (the "GPL"), or
  26. * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  27. * in which case the provisions of the GPL or the LGPL are applicable instead
  28. * of those above. If you wish to allow use of your version of this file only
  29. * under the terms of either the GPL or the LGPL, and not to allow others to
  30. * use your version of this file under the terms of the MPL, indicate your
  31. * decision by deleting the provisions above and replace them with the notice
  32. * and other provisions required by the GPL or the LGPL. If you do not delete
  33. * the provisions above, a recipient may use your version of this file under
  34. * the terms of any one of the MPL, the GPL or the LGPL.
  35. *
  36. * ***** END LICENSE BLOCK ***** */
  37. const Cc = Components.classes;
  38. const Ci = Components.interfaces;
  39. Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  40. Components.utils.import("resource://gre/modules/Services.jsm");
  41. function LoginManager() {
  42. this.init();
  43. }
  44. LoginManager.prototype = {
  45. classID: Components.ID("{cb9e0de8-3598-4ed7-857b-827f011ad5d8}"),
  46. QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManager,
  47. Ci.nsISupportsWeakReference,
  48. Ci.nsIInterfaceRequestor]),
  49. getInterface : function(aIID) {
  50. if (aIID.equals(Ci.mozIStorageConnection) && this._storage) {
  51. let ir = this._storage.QueryInterface(Ci.nsIInterfaceRequestor);
  52. return ir.getInterface(aIID);
  53. }
  54. throw Cr.NS_ERROR_NO_INTERFACE;
  55. },
  56. /* ---------- private memebers ---------- */
  57. __formFillService : null, // FormFillController, for username autocompleting
  58. get _formFillService() {
  59. if (!this.__formFillService)
  60. this.__formFillService =
  61. Cc["@mozilla.org/satchel/form-fill-controller;1"].
  62. getService(Ci.nsIFormFillController);
  63. return this.__formFillService;
  64. },
  65. __storage : null, // Storage component which contains the saved logins
  66. get _storage() {
  67. if (!this.__storage) {
  68. var contractID = "@mozilla.org/login-manager/storage/mozStorage;1";
  69. try {
  70. var catMan = Cc["@mozilla.org/categorymanager;1"].
  71. getService(Ci.nsICategoryManager);
  72. contractID = catMan.getCategoryEntry("login-manager-storage",
  73. "nsILoginManagerStorage");
  74. this.log("Found alternate nsILoginManagerStorage with " +
  75. "contract ID: " + contractID);
  76. } catch (e) {
  77. this.log("No alternate nsILoginManagerStorage registered");
  78. }
  79. this.__storage = Cc[contractID].
  80. createInstance(Ci.nsILoginManagerStorage);
  81. try {
  82. this.__storage.init();
  83. } catch (e) {
  84. this.log("Initialization of storage component failed: " + e);
  85. this.__storage = null;
  86. }
  87. }
  88. return this.__storage;
  89. },
  90. // Private Browsing Service
  91. // If the service is not available, null will be returned.
  92. __privateBrowsingService : undefined,
  93. get _privateBrowsingService() {
  94. if (this.__privateBrowsingService == undefined) {
  95. if ("@mozilla.org/privatebrowsing;1" in Cc)
  96. this.__privateBrowsingService = Cc["@mozilla.org/privatebrowsing;1"].
  97. getService(Ci.nsIPrivateBrowsingService);
  98. else
  99. this.__privateBrowsingService = null;
  100. }
  101. return this.__privateBrowsingService;
  102. },
  103. // Whether we are in private browsing mode
  104. get _inPrivateBrowsing() {
  105. var pbSvc = this._privateBrowsingService;
  106. if (pbSvc)
  107. return pbSvc.privateBrowsingEnabled;
  108. else
  109. return false;
  110. },
  111. _prefBranch : null, // Preferences service
  112. _nsLoginInfo : null, // Constructor for nsILoginInfo implementation
  113. _remember : true, // mirrors signon.rememberSignons preference
  114. _debug : false, // mirrors signon.debug
  115. /*
  116. * init
  117. *
  118. * Initialize the Login Manager. Automatically called when service
  119. * is created.
  120. *
  121. * Note: Service created in /browser/base/content/browser.js,
  122. * delayedStartup()
  123. */
  124. init : function () {
  125. // Cache references to current |this| in utility objects
  126. this._webProgressListener._domEventListener = this._domEventListener;
  127. this._webProgressListener._pwmgr = this;
  128. this._domEventListener._pwmgr = this;
  129. this._observer._pwmgr = this;
  130. // Preferences. Add observer so we get notified of changes.
  131. this._prefBranch = Services.prefs.getBranch("signon.");
  132. this._prefBranch.addObserver("", this._observer, false);
  133. // Get current preference values.
  134. this._debug = this._prefBranch.getBoolPref("debug");
  135. this._remember = this._prefBranch.getBoolPref("rememberSignons");
  136. // Get constructor for nsILoginInfo
  137. this._nsLoginInfo = new Components.Constructor(
  138. "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo);
  139. // Form submit observer checks forms for new logins and pw changes.
  140. Services.obs.addObserver(this._observer, "earlyformsubmit", false);
  141. Services.obs.addObserver(this._observer, "xpcom-shutdown", false);
  142. // WebProgressListener for getting notification of new doc loads.
  143. var progress = Cc["@mozilla.org/docloaderservice;1"].
  144. getService(Ci.nsIWebProgress);
  145. progress.addProgressListener(this._webProgressListener,
  146. Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
  147. },
  148. /*
  149. * log
  150. *
  151. * Internal function for logging debug messages to the Error Console window
  152. */
  153. log : function (message) {
  154. if (!this._debug)
  155. return;
  156. dump("Login Manager: " + message + "\n");
  157. Services.console.logStringMessage("Login Manager: " + message);
  158. },
  159. /* ---------- Utility objects ---------- */
  160. /*
  161. * _observer object
  162. *
  163. * Internal utility object, implements the nsIObserver interface.
  164. * Used to receive notification for: form submission, preference changes.
  165. */
  166. _observer : {
  167. _pwmgr : null,
  168. QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
  169. Ci.nsIFormSubmitObserver,
  170. Ci.nsISupportsWeakReference]),
  171. // nsFormSubmitObserver
  172. notify : function (formElement, aWindow, actionURI) {
  173. this._pwmgr.log("observer notified for form submission.");
  174. // We're invoked before the content's |onsubmit| handlers, so we
  175. // can grab form data before it might be modified (see bug 257781).
  176. try {
  177. this._pwmgr._onFormSubmit(formElement);
  178. } catch (e) {
  179. this._pwmgr.log("Caught error in onFormSubmit: " + e);
  180. }
  181. return true; // Always return true, or form submit will be canceled.
  182. },
  183. // nsObserver
  184. observe : function (subject, topic, data) {
  185. if (topic == "nsPref:changed") {
  186. var prefName = data;
  187. this._pwmgr.log("got change to " + prefName + " preference");
  188. if (prefName == "debug") {
  189. this._pwmgr._debug =
  190. this._pwmgr._prefBranch.getBoolPref("debug");
  191. } else if (prefName == "rememberSignons") {
  192. this._pwmgr._remember =
  193. this._pwmgr._prefBranch.getBoolPref("rememberSignons");
  194. } else {
  195. this._pwmgr.log("Oops! Pref not handled, change ignored.");
  196. }
  197. } else if (topic == "xpcom-shutdown") {
  198. for (let i in this._pwmgr) {
  199. try {
  200. this._pwmgr[i] = null;
  201. } catch(ex) {}
  202. }
  203. this._pwmgr = null;
  204. } else {
  205. this._pwmgr.log("Oops! Unexpected notification: " + topic);
  206. }
  207. }
  208. },
  209. /*
  210. * _webProgressListener object
  211. *
  212. * Internal utility object, implements nsIWebProgressListener interface.
  213. * This is attached to the document loader service, so we get
  214. * notifications about all page loads.
  215. */
  216. _webProgressListener : {
  217. _pwmgr : null,
  218. _domEventListener : null,
  219. QueryInterface : XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
  220. Ci.nsISupportsWeakReference]),
  221. onStateChange : function (aWebProgress, aRequest,
  222. aStateFlags, aStatus) {
  223. // STATE_START is too early, doc is still the old page.
  224. if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_TRANSFERRING))
  225. return;
  226. if (!this._pwmgr._remember)
  227. return;
  228. var domWin = aWebProgress.DOMWindow;
  229. var domDoc = domWin.document;
  230. // Only process things which might have HTML forms.
  231. if (!(domDoc instanceof Ci.nsIDOMHTMLDocument))
  232. return;
  233. if (this._pwmgr._debug) {
  234. let requestName = "(null)";
  235. if (aRequest) {
  236. try {
  237. requestName = aRequest.name;
  238. } catch (ex if ex.result == Components.results.NS_ERROR_NOT_IMPLEMENTED) {
  239. // do nothing - leave requestName = "(null)"
  240. }
  241. }
  242. this._pwmgr.log("onStateChange accepted: req = " + requestName +
  243. ", flags = 0x" + aStateFlags.toString(16));
  244. }
  245. // Fastback doesn't fire DOMContentLoaded, so process forms now.
  246. if (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) {
  247. this._pwmgr.log("onStateChange: restoring document");
  248. return this._pwmgr._fillDocument(domDoc);
  249. }
  250. // Add event listener to process page when DOM is complete.
  251. domDoc.addEventListener("DOMContentLoaded",
  252. this._domEventListener, false);
  253. return;
  254. },
  255. // stubs for the nsIWebProgressListener interfaces which we don't use.
  256. onProgressChange : function() { throw "Unexpected onProgressChange"; },
  257. onLocationChange : function() { throw "Unexpected onLocationChange"; },
  258. onStatusChange : function() { throw "Unexpected onStatusChange"; },
  259. onSecurityChange : function() { throw "Unexpected onSecurityChange"; }
  260. },
  261. /*
  262. * _domEventListener object
  263. *
  264. * Internal utility object, implements nsIDOMEventListener
  265. * Used to catch certain DOM events needed to properly implement form fill.
  266. */
  267. _domEventListener : {
  268. _pwmgr : null,
  269. QueryInterface : XPCOMUtils.generateQI([Ci.nsIDOMEventListener,
  270. Ci.nsISupportsWeakReference]),
  271. handleEvent : function (event) {
  272. if (!event.isTrusted)
  273. return;
  274. this._pwmgr.log("domEventListener: got event " + event.type);
  275. switch (event.type) {
  276. case "DOMContentLoaded":
  277. this._pwmgr._fillDocument(event.target);
  278. return;
  279. case "DOMAutoComplete":
  280. case "blur":
  281. var acInputField = event.target;
  282. var acForm = acInputField.form;
  283. // If the username is blank, bail out now -- we don't want
  284. // fillForm() to try filling in a login without a username
  285. // to filter on (bug 471906).
  286. if (!acInputField.value)
  287. return;
  288. // Make sure the username field fillForm will use is the
  289. // same field as the autocomplete was activated on. If
  290. // not, the DOM has been altered and we'll just give up.
  291. var [usernameField, passwordField, ignored] =
  292. this._pwmgr._getFormFields(acForm, false);
  293. if (usernameField == acInputField && passwordField) {
  294. // This shouldn't trigger a master password prompt,
  295. // because we don't attach to the input until after we
  296. // successfully obtain logins for the form.
  297. this._pwmgr._fillForm(acForm, true, true, true, null);
  298. } else {
  299. this._pwmgr.log("Oops, form changed before AC invoked");
  300. }
  301. return;
  302. default:
  303. this._pwmgr.log("Oops! This event unexpected.");
  304. return;
  305. }
  306. }
  307. },
  308. /* ---------- Primary Public interfaces ---------- */
  309. /*
  310. * addLogin
  311. *
  312. * Add a new login to login storage.
  313. */
  314. addLogin : function (login) {
  315. // Sanity check the login
  316. if (login.hostname == null || login.hostname.length == 0)
  317. throw "Can't add a login with a null or empty hostname.";
  318. // For logins w/o a username, set to "", not null.
  319. if (login.username == null)
  320. throw "Can't add a login with a null username.";
  321. if (login.password == null || login.password.length == 0)
  322. throw "Can't add a login with a null or empty password.";
  323. if (login.formSubmitURL || login.formSubmitURL == "") {
  324. // We have a form submit URL. Can't have a HTTP realm.
  325. if (login.httpRealm != null)
  326. throw "Can't add a login with both a httpRealm and formSubmitURL.";
  327. } else if (login.httpRealm) {
  328. // We have a HTTP realm. Can't have a form submit URL.
  329. if (login.formSubmitURL != null)
  330. throw "Can't add a login with both a httpRealm and formSubmitURL.";
  331. } else {
  332. // Need one or the other!
  333. throw "Can't add a login without a httpRealm or formSubmitURL.";
  334. }
  335. // Look for an existing entry.
  336. var logins = this.findLogins({}, login.hostname, login.formSubmitURL,
  337. login.httpRealm);
  338. if (logins.some(function(l) login.matches(l, true)))
  339. throw "This login already exists.";
  340. this.log("Adding login: " + login);
  341. return this._storage.addLogin(login);
  342. },
  343. /*
  344. * removeLogin
  345. *
  346. * Remove the specified login from the stored logins.
  347. */
  348. removeLogin : function (login) {
  349. this.log("Removing login: " + login);
  350. return this._storage.removeLogin(login);
  351. },
  352. /*
  353. * modifyLogin
  354. *
  355. * Change the specified login to match the new login.
  356. */
  357. modifyLogin : function (oldLogin, newLogin) {
  358. this.log("Modifying oldLogin: " + oldLogin + " newLogin: " + newLogin);
  359. return this._storage.modifyLogin(oldLogin, newLogin);
  360. },
  361. /*
  362. * getAllLogins
  363. *
  364. * Get a dump of all stored logins. Used by the login manager UI.
  365. *
  366. * |count| is only needed for XPCOM.
  367. *
  368. * Returns an array of logins. If there are no logins, the array is empty.
  369. */
  370. getAllLogins : function (count) {
  371. this.log("Getting a list of all logins");
  372. return this._storage.getAllLogins(count);
  373. },
  374. /*
  375. * removeAllLogins
  376. *
  377. * Remove all stored logins.
  378. */
  379. removeAllLogins : function () {
  380. this.log("Removing all logins");
  381. this._storage.removeAllLogins();
  382. },
  383. /*
  384. * getAllDisabledHosts
  385. *
  386. * Get a list of all hosts for which logins are disabled.
  387. *
  388. * |count| is only needed for XPCOM.
  389. *
  390. * Returns an array of disabled logins. If there are no disabled logins,
  391. * the array is empty.
  392. */
  393. getAllDisabledHosts : function (count) {
  394. this.log("Getting a list of all disabled hosts");
  395. return this._storage.getAllDisabledHosts(count);
  396. },
  397. /*
  398. * findLogins
  399. *
  400. * Search for the known logins for entries matching the specified criteria.
  401. */
  402. findLogins : function (count, hostname, formSubmitURL, httpRealm) {
  403. this.log("Searching for logins matching host: " + hostname +
  404. ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm);
  405. return this._storage.findLogins(count, hostname, formSubmitURL,
  406. httpRealm);
  407. },
  408. /*
  409. * searchLogins
  410. *
  411. * Public wrapper around _searchLogins to convert the nsIPropertyBag to a
  412. * JavaScript object and decrypt the results.
  413. *
  414. * Returns an array of decrypted nsILoginInfo.
  415. */
  416. searchLogins : function(count, matchData) {
  417. this.log("Searching for logins");
  418. return this._storage.searchLogins(count, matchData);
  419. },
  420. /*
  421. * countLogins
  422. *
  423. * Search for the known logins for entries matching the specified criteria,
  424. * returns only the count.
  425. */
  426. countLogins : function (hostname, formSubmitURL, httpRealm) {
  427. this.log("Counting logins matching host: " + hostname +
  428. ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm);
  429. return this._storage.countLogins(hostname, formSubmitURL, httpRealm);
  430. },
  431. /*
  432. * uiBusy
  433. */
  434. get uiBusy() {
  435. return this._storage.uiBusy;
  436. },
  437. /*
  438. * getLoginSavingEnabled
  439. *
  440. * Check to see if user has disabled saving logins for the host.
  441. */
  442. getLoginSavingEnabled : function (host) {
  443. this.log("Checking if logins to " + host + " can be saved.");
  444. if (!this._remember)
  445. return false;
  446. return this._storage.getLoginSavingEnabled(host);
  447. },
  448. /*
  449. * setLoginSavingEnabled
  450. *
  451. * Enable or disable storing logins for the specified host.
  452. */
  453. setLoginSavingEnabled : function (hostname, enabled) {
  454. // Nulls won't round-trip with getAllDisabledHosts().
  455. if (hostname.indexOf("\0") != -1)
  456. throw "Invalid hostname";
  457. this.log("Saving logins for " + hostname + " enabled? " + enabled);
  458. return this._storage.setLoginSavingEnabled(hostname, enabled);
  459. },
  460. /*
  461. * autoCompleteSearch
  462. *
  463. * Yuck. This is called directly by satchel:
  464. * nsFormFillController::StartSearch()
  465. * [toolkit/components/satchel/src/nsFormFillController.cpp]
  466. *
  467. * We really ought to have a simple way for code to register an
  468. * auto-complete provider, and not have satchel calling pwmgr directly.
  469. */
  470. autoCompleteSearch : function (aSearchString, aPreviousResult, aElement) {
  471. // aPreviousResult & aResult are nsIAutoCompleteResult,
  472. // aElement is nsIDOMHTMLInputElement
  473. if (!this._remember)
  474. return null;
  475. this.log("AutoCompleteSearch invoked. Search is: " + aSearchString);
  476. var result = null;
  477. if (aPreviousResult &&
  478. aSearchString.substr(0, aPreviousResult.searchString.length) == aPreviousResult.searchString) {
  479. this.log("Using previous autocomplete result");
  480. result = aPreviousResult;
  481. result.wrappedJSObject.searchString = aSearchString;
  482. // We have a list of results for a shorter search string, so just
  483. // filter them further based on the new search string.
  484. // Count backwards, because result.matchCount is decremented
  485. // when we remove an entry.
  486. for (var i = result.matchCount - 1; i >= 0; i--) {
  487. var match = result.getValueAt(i);
  488. // Remove results that are too short, or have different prefix.
  489. if (aSearchString.length > match.length ||
  490. aSearchString.toLowerCase() !=
  491. match.substr(0, aSearchString.length).toLowerCase())
  492. {
  493. this.log("Removing autocomplete entry '" + match + "'");
  494. result.removeValueAt(i, false);
  495. }
  496. }
  497. } else {
  498. this.log("Creating new autocomplete search result.");
  499. var doc = aElement.ownerDocument;
  500. var origin = this._getPasswordOrigin(doc.documentURI);
  501. var actionOrigin = this._getActionOrigin(aElement.form);
  502. // This shouldn't trigger a master password prompt, because we
  503. // don't attach to the input until after we successfully obtain
  504. // logins for the form.
  505. var logins = this.findLogins({}, origin, actionOrigin, null);
  506. var matchingLogins = [];
  507. // Filter out logins that don't match the search prefix. Also
  508. // filter logins without a username, since that's confusing to see
  509. // in the dropdown and we can't autocomplete them anyway.
  510. for (i = 0; i < logins.length; i++) {
  511. var username = logins[i].username.toLowerCase();
  512. if (username &&
  513. aSearchString.length <= username.length &&
  514. aSearchString.toLowerCase() ==
  515. username.substr(0, aSearchString.length))
  516. {
  517. matchingLogins.push(logins[i]);
  518. }
  519. }
  520. this.log(matchingLogins.length + " autocomplete logins avail.");
  521. result = new UserAutoCompleteResult(aSearchString, matchingLogins);
  522. }
  523. return result;
  524. },
  525. /* ------- Internal methods / callbacks for document integration ------- */
  526. /*
  527. * _getPasswordFields
  528. *
  529. * Returns an array of password field elements for the specified form.
  530. * If no pw fields are found, or if more than 3 are found, then null
  531. * is returned.
  532. *
  533. * skipEmptyFields can be set to ignore password fields with no value.
  534. */
  535. _getPasswordFields : function (form, skipEmptyFields) {
  536. // Locate the password fields in the form.
  537. var pwFields = [];
  538. for (var i = 0; i < form.elements.length; i++) {
  539. var element = form.elements[i];
  540. if (!(element instanceof Ci.nsIDOMHTMLInputElement) ||
  541. element.type != "password")
  542. continue;
  543. if (skipEmptyFields && !element.value)
  544. continue;
  545. pwFields[pwFields.length] = {
  546. index : i,
  547. element : element
  548. };
  549. }
  550. // If too few or too many fields, bail out.
  551. if (pwFields.length == 0) {
  552. this.log("(form ignored -- no password fields.)");
  553. return null;
  554. } else if (pwFields.length > 3) {
  555. this.log("(form ignored -- too many password fields. [got " +
  556. pwFields.length + "])");
  557. return null;
  558. }
  559. return pwFields;
  560. },
  561. /*
  562. * _getFormFields
  563. *
  564. * Returns the username and password fields found in the form.
  565. * Can handle complex forms by trying to figure out what the
  566. * relevant fields are.
  567. *
  568. * Returns: [usernameField, newPasswordField, oldPasswordField]
  569. *
  570. * usernameField may be null.
  571. * newPasswordField will always be non-null.
  572. * oldPasswordField may be null. If null, newPasswordField is just
  573. * "theLoginField". If not null, the form is apparently a
  574. * change-password field, with oldPasswordField containing the password
  575. * that is being changed.
  576. */
  577. _getFormFields : function (form, isSubmission) {
  578. var usernameField = null;
  579. // Locate the password field(s) in the form. Up to 3 supported.
  580. // If there's no password field, there's nothing for us to do.
  581. var pwFields = this._getPasswordFields(form, isSubmission);
  582. if (!pwFields)
  583. return [null, null, null];
  584. // Locate the username field in the form by searching backwards
  585. // from the first passwordfield, assume the first text field is the
  586. // username. We might not find a username field if the user is
  587. // already logged in to the site.
  588. for (var i = pwFields[0].index - 1; i >= 0; i--) {
  589. var element = form.elements[i];
  590. var fieldType = (element.hasAttribute("type") ?
  591. element.getAttribute("type").toLowerCase() :
  592. element.type);
  593. if (fieldType == "text" ||
  594. fieldType == "email" ||
  595. fieldType == "url" ||
  596. fieldType == "tel" ||
  597. fieldType == "number") {
  598. usernameField = element;
  599. break;
  600. }
  601. }
  602. if (!usernameField)
  603. this.log("(form -- no username field found)");
  604. // If we're not submitting a form (it's a page load), there are no
  605. // password field values for us to use for identifying fields. So,
  606. // just assume the first password field is the one to be filled in.
  607. if (!isSubmission || pwFields.length == 1)
  608. return [usernameField, pwFields[0].element, null];
  609. // Try to figure out WTF is in the form based on the password values.
  610. var oldPasswordField, newPasswordField;
  611. var pw1 = pwFields[0].element.value;
  612. var pw2 = pwFields[1].element.value;
  613. var pw3 = (pwFields[2] ? pwFields[2].element.value : null);
  614. if (pwFields.length == 3) {
  615. // Look for two identical passwords, that's the new password
  616. if (pw1 == pw2 && pw2 == pw3) {
  617. // All 3 passwords the same? Weird! Treat as if 1 pw field.
  618. newPasswordField = pwFields[0].element;
  619. oldPasswordField = null;
  620. } else if (pw1 == pw2) {
  621. newPasswordField = pwFields[0].element;
  622. oldPasswordField = pwFields[2].element;
  623. } else if (pw2 == pw3) {
  624. oldPasswordField = pwFields[0].element;
  625. newPasswordField = pwFields[2].element;
  626. } else if (pw1 == pw3) {
  627. // A bit odd, but could make sense with the right page layout.
  628. newPasswordField = pwFields[0].element;
  629. oldPasswordField = pwFields[1].element;
  630. } else {
  631. // We can't tell which of the 3 passwords should be saved.
  632. this.log("(form ignored -- all 3 pw fields differ)");
  633. return [null, null, null];
  634. }
  635. } else { // pwFields.length == 2
  636. if (pw1 == pw2) {
  637. // Treat as if 1 pw field
  638. newPasswordField = pwFields[0].element;
  639. oldPasswordField = null;
  640. } else {
  641. // Just assume that the 2nd password is the new password
  642. oldPasswordField = pwFields[0].element;
  643. newPasswordField = pwFields[1].element;
  644. }
  645. }
  646. return [usernameField, newPasswordField, oldPasswordField];
  647. },
  648. /*
  649. * _isAutoCompleteDisabled
  650. *
  651. * Returns true if the page requests autocomplete be disabled for the
  652. * specified form input.
  653. */
  654. _isAutocompleteDisabled : function (element) {
  655. if (element && element.hasAttribute("autocomplete") &&
  656. element.getAttribute("autocomplete").toLowerCase() == "off")
  657. return true;
  658. return false;
  659. },
  660. /*
  661. * _onFormSubmit
  662. *
  663. * Called by the our observer when notified of a form submission.
  664. * [Note that this happens before any DOM onsubmit handlers are invoked.]
  665. * Looks for a password change in the submitted form, so we can update
  666. * our stored password.
  667. */
  668. _onFormSubmit : function (form) {
  669. // local helper function
  670. function getPrompter(aWindow) {
  671. var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"].
  672. createInstance(Ci.nsILoginManagerPrompter);
  673. prompterSvc.init(aWindow);
  674. return prompterSvc;
  675. }
  676. if (this._inPrivateBrowsing) {
  677. // We won't do anything in private browsing mode anyway,
  678. // so there's no need to perform further checks.
  679. this.log("(form submission ignored in private browsing mode)");
  680. return;
  681. }
  682. var doc = form.ownerDocument;
  683. var win = doc.defaultView;
  684. // If password saving is disabled (globally or for host), bail out now.
  685. if (!this._remember)
  686. return;
  687. var hostname = this._getPasswordOrigin(doc.documentURI);
  688. if (!hostname) {
  689. this.log("(form submission ignored -- invalid hostname)");
  690. return;
  691. }
  692. var formSubmitURL = this._getActionOrigin(form)
  693. if (!this.getLoginSavingEnabled(hostname)) {
  694. this.log("(form submission ignored -- saving is " +
  695. "disabled for: " + hostname + ")");
  696. return;
  697. }
  698. // Get the appropriate fields from the form.
  699. var [usernameField, newPasswordField, oldPasswordField] =
  700. this._getFormFields(form, true);
  701. // Need at least 1 valid password field to do anything.
  702. if (newPasswordField == null)
  703. return;
  704. // Check for autocomplete=off attribute. We don't use it to prevent
  705. // autofilling (for existing logins), but won't save logins when it's
  706. // present.
  707. // XXX spin out a bug that we don't update timeLastUsed in this case?
  708. if (this._isAutocompleteDisabled(form) ||
  709. this._isAutocompleteDisabled(usernameField) ||
  710. this._isAutocompleteDisabled(newPasswordField) ||
  711. this._isAutocompleteDisabled(oldPasswordField)) {
  712. this.log("(form submission ignored -- autocomplete=off found)");
  713. return;
  714. }
  715. var formLogin = new this._nsLoginInfo();
  716. formLogin.init(hostname, formSubmitURL, null,
  717. (usernameField ? usernameField.value : ""),
  718. newPasswordField.value,
  719. (usernameField ? usernameField.name : ""),
  720. newPasswordField.name);
  721. // If we didn't find a username field, but seem to be changing a
  722. // password, allow the user to select from a list of applicable
  723. // logins to update the password for.
  724. if (!usernameField && oldPasswordField) {
  725. var logins = this.findLogins({}, hostname, formSubmitURL, null);
  726. if (logins.length == 0) {
  727. // Could prompt to save this as a new password-only login.
  728. // This seems uncommon, and might be wrong, so ignore.
  729. this.log("(no logins for this host -- pwchange ignored)");
  730. return;
  731. }
  732. var prompter = getPrompter(win);
  733. if (logins.length == 1) {
  734. var oldLogin = logins[0];
  735. formLogin.username = oldLogin.username;
  736. formLogin.usernameField = oldLogin.usernameField;
  737. prompter.promptToChangePassword(oldLogin, formLogin);
  738. } else {
  739. prompter.promptToChangePasswordWithUsernames(
  740. logins, logins.length, formLogin);
  741. }
  742. return;
  743. }
  744. // Look for an existing login that matches the form login.
  745. var existingLogin = null;
  746. var logins = this.findLogins({}, hostname, formSubmitURL, null);
  747. for (var i = 0; i < logins.length; i++) {
  748. var same, login = logins[i];
  749. // If one login has a username but the other doesn't, ignore
  750. // the username when comparing and only match if they have the
  751. // same password. Otherwise, compare the logins and match even
  752. // if the passwords differ.
  753. if (!login.username && formLogin.username) {
  754. var restoreMe = formLogin.username;
  755. formLogin.username = "";
  756. same = formLogin.matches(login, false);
  757. formLogin.username = restoreMe;
  758. } else if (!formLogin.username && login.username) {
  759. formLogin.username = login.username;
  760. same = formLogin.matches(login, false);
  761. formLogin.username = ""; // we know it's always blank.
  762. } else {
  763. same = formLogin.matches(login, true);
  764. }
  765. if (same) {
  766. existingLogin = login;
  767. break;
  768. }
  769. }
  770. if (existingLogin) {
  771. this.log("Found an existing login matching this form submission");
  772. // Change password if needed.
  773. if (existingLogin.password != formLogin.password) {
  774. this.log("...passwords differ, prompting to change.");
  775. prompter = getPrompter(win);
  776. prompter.promptToChangePassword(existingLogin, formLogin);
  777. } else {
  778. // Update the lastUsed timestamp.
  779. var propBag = Cc["@mozilla.org/hash-property-bag;1"].
  780. createInstance(Ci.nsIWritablePropertyBag);
  781. propBag.setProperty("timeLastUsed", Date.now());
  782. propBag.setProperty("timesUsedIncrement", 1);
  783. this.modifyLogin(existingLogin, propBag);
  784. }
  785. return;
  786. }
  787. // Prompt user to save login (via dialog or notification bar)
  788. prompter = getPrompter(win);
  789. prompter.promptToSavePassword(formLogin);
  790. },
  791. /*
  792. * _getPasswordOrigin
  793. *
  794. * Get the parts of the URL we want for identification.
  795. */
  796. _getPasswordOrigin : function (uriString, allowJS) {
  797. var realm = "";
  798. try {
  799. var uri = Services.io.newURI(uriString, null, null);
  800. if (allowJS && uri.scheme == "javascript")
  801. return "javascript:"
  802. realm = uri.scheme + "://" + uri.host;
  803. // If the URI explicitly specified a port, only include it when
  804. // it's not the default. (We never want "http://foo.com:80")
  805. var port = uri.port;
  806. if (port != -1) {
  807. var handler = Services.io.getProtocolHandler(uri.scheme);
  808. if (port != handler.defaultPort)
  809. realm += ":" + port;
  810. }
  811. } catch (e) {
  812. // bug 159484 - disallow url types that don't support a hostPort.
  813. // (although we handle "javascript:..." as a special case above.)
  814. this.log("Couldn't parse origin for " + uriString);
  815. realm = null;
  816. }
  817. return realm;
  818. },
  819. _getActionOrigin : function (form) {
  820. var uriString = form.action;
  821. // A blank or missing action submits to where it came from.
  822. if (uriString == "")
  823. uriString = form.baseURI; // ala bug 297761
  824. return this._getPasswordOrigin(uriString, true);
  825. },
  826. /*
  827. * _fillDocument
  828. *
  829. * Called when a page has loaded. For each form in the document,
  830. * we check to see if it can be filled with a stored login.
  831. */
  832. _fillDocument : function (doc) {
  833. var forms = doc.forms;
  834. if (!forms || forms.length == 0)
  835. return;
  836. var formOrigin = this._getPasswordOrigin(doc.documentURI);
  837. // If there are no logins for this site, bail out now.
  838. if (!this.countLogins(formOrigin, "", null))
  839. return;
  840. // If we're currently displaying a master password prompt, defer
  841. // processing this document until the user handles the prompt.
  842. if (this.uiBusy) {
  843. this.log("deferring fillDoc for " + doc.documentURI);
  844. let self = this;
  845. let observer = {
  846. QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
  847. observe: function (subject, topic, data) {
  848. self.log("Got deferred fillDoc notification: " + topic);
  849. // Only run observer once.
  850. Services.obs.removeObserver(this, "passwordmgr-crypto-login");
  851. Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled");
  852. if (topic == "passwordmgr-crypto-loginCanceled")
  853. return;
  854. self._fillDocument(doc);
  855. },
  856. handleEvent : function (event) {
  857. // Not expected to be called
  858. }
  859. };
  860. // Trickyness follows: We want an observer, but don't want it to
  861. // cause leaks. So add the observer with a weak reference, and use
  862. // a dummy event listener (a strong reference) to keep it alive
  863. // until the document is destroyed.
  864. Services.obs.addObserver(observer, "passwordmgr-crypto-login", true);
  865. Services.obs.addObserver(observer, "passwordmgr-crypto-loginCanceled", true);
  866. doc.addEventListener("mozCleverClosureHack", observer, false);
  867. return;
  868. }
  869. this.log("fillDocument processing " + forms.length +
  870. " forms on " + doc.documentURI);
  871. var autofillForm = !this._inPrivateBrowsing &&
  872. this._prefBranch.getBoolPref("autofillForms");
  873. var previousActionOrigin = null;
  874. var foundLogins = null;
  875. for (var i = 0; i < forms.length; i++) {
  876. var form = forms[i];
  877. // Only the actionOrigin might be changing, so if it's the same
  878. // as the last form on the page we can reuse the same logins.
  879. var actionOrigin = this._getActionOrigin(form);
  880. if (actionOrigin != previousActionOrigin) {
  881. foundLogins = null;
  882. previousActionOrigin = actionOrigin;
  883. }
  884. this.log("_fillDocument processing form[" + i + "]");
  885. foundLogins = this._fillForm(form, autofillForm, false, false, foundLogins)[1];
  886. } // foreach form
  887. },
  888. /*
  889. * _fillform
  890. *
  891. * Fill the form with login information if we can find it. This will find
  892. * an array of logins if not given any, otherwise it will use the logins
  893. * passed in. The logins are returned so they can be reused for
  894. * optimization. Success of action is also returned in format
  895. * [success, foundLogins]. autofillForm denotes if we should fill the form
  896. * in automatically, ignoreAutocomplete denotes if we should ignore
  897. * autocomplete=off attributes, and foundLogins is an array of nsILoginInfo
  898. * for optimization
  899. */
  900. _fillForm : function (form, autofillForm, ignoreAutocomplete,
  901. clobberPassword, foundLogins) {
  902. // Heuristically determine what the user/pass fields are
  903. // We do this before checking to see if logins are stored,
  904. // so that the user isn't prompted for a master password
  905. // without need.
  906. var [usernameField, passwordField, ignored] =
  907. this._getFormFields(form, false);
  908. // Need a valid password field to do anything.
  909. if (passwordField == null)
  910. return [false, foundLogins];
  911. // If the fields are disabled or read-only, there's nothing to do.
  912. if (passwordField.disabled || passwordField.readOnly ||
  913. usernameField && (usernameField.disabled ||
  914. usernameField.readOnly)) {
  915. this.log("not filling form, login fields disabled");
  916. return [false, foundLogins];
  917. }
  918. // Need to get a list of logins if we weren't given them
  919. if (foundLogins == null) {
  920. var formOrigin =
  921. this._getPasswordOrigin(form.ownerDocument.documentURI);
  922. var actionOrigin = this._getActionOrigin(form);
  923. foundLogins = this.findLogins({}, formOrigin, actionOrigin, null);
  924. this.log("found " + foundLogins.length + " matching logins.");
  925. } else {
  926. this.log("reusing logins from last form.");
  927. }
  928. // Discard logins which have username/password values that don't
  929. // fit into the fields (as specified by the maxlength attribute).
  930. // The user couldn't enter these values anyway, and it helps
  931. // with sites that have an extra PIN to be entered (bug 391514)
  932. var maxUsernameLen = Number.MAX_VALUE;
  933. var maxPasswordLen = Number.MAX_VALUE;
  934. // If attribute wasn't set, default is -1.
  935. if (usernameField && usernameField.maxLength >= 0)
  936. maxUsernameLen = usernameField.maxLength;
  937. if (passwordField.maxLength >= 0)
  938. maxPasswordLen = passwordField.maxLength;
  939. var logins = foundLogins.filter(function (l) {
  940. var fit = (l.username.length <= maxUsernameLen &&
  941. l.password.length <= maxPasswordLen);
  942. if (!fit)
  943. this.log("Ignored " + l.username + " login: won't fit");
  944. return fit;
  945. }, this);
  946. // Nothing to do if we have no matching logins available.
  947. if (logins.length == 0)
  948. return [false, foundLogins];
  949. // The reason we didn't end up filling the form, if any. We include
  950. // this in the formInfo object we send with the passwordmgr-found-logins
  951. // notification. See the _notifyFoundLogins docs for possible values.
  952. var didntFillReason = null;
  953. // Attach autocomplete stuff to the username field, if we have
  954. // one. This is normally used to select from multiple accounts,
  955. // but even with one account we should refill if the user edits.
  956. if (usernameField)
  957. this._attachToInput(usernameField);
  958. // Don't clobber an existing password.
  959. if (passwordField.value && !clobberPassword) {
  960. didntFillReason = "existingPassword";
  961. this._notifyFoundLogins(didntFillReason, usernameField,
  962. passwordField, foundLogins, null);
  963. return [false, foundLogins];
  964. }
  965. // If the form has an autocomplete=off attribute in play, don't
  966. // fill in the login automatically. We check this after attaching
  967. // the autocomplete stuff to the username field, so the user can
  968. // still manually select a login to be filled in.
  969. var isFormDisabled = false;
  970. if (!ignoreAutocomplete &&
  971. (this._isAutocompleteDisabled(form) ||
  972. this._isAutocompleteDisabled(usernameField) ||
  973. this._isAutocompleteDisabled(passwordField))) {
  974. isFormDisabled = true;
  975. this.log("form not filled, has autocomplete=off");
  976. }
  977. // Variable such that we reduce code duplication and can be sure we
  978. // should be firing notifications if and only if we can fill the form.
  979. var selectedLogin = null;
  980. if (usernameField && usernameField.value) {
  981. // If username was specified in the form, only fill in the
  982. // password if we find a matching login.
  983. var username = usernameField.value.toLowerCase();
  984. let matchingLogins = logins.filter(function(l)
  985. l.username.toLowerCase() == username);
  986. if (matchingLogins.length) {
  987. selectedLogin = matchingLogins[0];
  988. } else {
  989. didntFillReason = "existingUsername";
  990. this.log("Password not filled. None of the stored " +
  991. "logins match the username already present.");
  992. }
  993. } else if (logins.length == 1) {
  994. selectedLogin = logins[0];
  995. } else {
  996. // We have multiple logins. Handle a special case here, for sites
  997. // which have a normal user+pass login *and* a password-only login
  998. // (eg, a PIN). Prefer the login that matches the type of the form
  999. // (user+pass or pass-only) when there's exactly one that matches.
  1000. let matchingLogins;
  1001. if (usernameField)
  1002. matchingLogins = logins.filter(function(l) l.username);
  1003. else
  1004. matchingLogins = logins.filter(function(l) !l.username);
  1005. if (matchingLogins.length == 1) {
  1006. selectedLogin = matchingLogins[0];
  1007. } else {
  1008. didntFillReason = "multipleLogins";
  1009. this.log("Multiple logins for form, so not filling any.");
  1010. }
  1011. }
  1012. var didFillForm = false;
  1013. if (selectedLogin && autofillForm && !isFormDisabled) {
  1014. // Fill the form
  1015. if (usernameField)
  1016. usernameField.value = selectedLogin.username;
  1017. passwordField.value = selectedLogin.password;
  1018. didFillForm = true;
  1019. } else if (selectedLogin && !autofillForm) {
  1020. // For when autofillForm is false, but we still have the information
  1021. // to fill a form, we notify observers.
  1022. didntFillReason = "noAutofillForms";
  1023. Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason);
  1024. this.log("autofillForms=false but form can be filled; notified observers");
  1025. } else if (selectedLogin && isFormDisabled) {
  1026. // For when autocomplete is off, but we still have the information
  1027. // to fill a form, we notify observers.
  1028. didntFillReason = "autocompleteOff";
  1029. Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason);
  1030. this.log("autocomplete=off but form can be filled; notified observers");
  1031. }
  1032. this._notifyFoundLogins(didntFillReason, usernameField, passwordField,
  1033. foundLogins, selectedLogin);
  1034. return [didFillForm, foundLogins];
  1035. },
  1036. /**
  1037. * Notify observers about an attempt to fill a form that resulted in some
  1038. * saved logins being found for the form.
  1039. *
  1040. * This does not get called if the login manager attempts to fill a form
  1041. * but does not find any saved logins. It does, however, get called when
  1042. * the login manager does find saved logins whether or not it actually
  1043. * fills the form with one of them.
  1044. *
  1045. * @param didntFillReason {String}
  1046. * the reason the login manager didn't fill the form, if any;
  1047. * if the value of this parameter is null, then the form was filled;
  1048. * otherwise, this parameter will be one of these values:
  1049. * existingUsername: the username field already contains a username
  1050. * that doesn't match any stored usernames
  1051. * existingPassword: the password field already contains a password
  1052. * autocompleteOff: autocomplete has been disabled for the form
  1053. * or its username or password fields
  1054. * multipleLogins: we have multiple logins for the form
  1055. * noAutofillForms: the autofillForms pref is set to false
  1056. *
  1057. * @param usernameField {HTMLInputElement}
  1058. * the username field detected by the login manager, if any;
  1059. * otherwise null
  1060. *
  1061. * @param passwordField {HTMLInputElement}
  1062. * the password field detected by the login manager
  1063. *
  1064. * @param foundLogins {Array}
  1065. * an array of nsILoginInfos that can be used to fill the form
  1066. *
  1067. * @param selectedLogin {nsILoginInfo}
  1068. * the nsILoginInfo that was/would be used to fill the form, if any;
  1069. * otherwise null; whether or not it was actually used depends on
  1070. * the value of the didntFillReason parameter
  1071. */
  1072. _notifyFoundLogins : function (didntFillReason, usernameField,
  1073. passwordField, foundLogins, selectedLogin) {
  1074. // We need .setProperty(), which is a method on the original
  1075. // nsIWritablePropertyBag. Strangley enough, nsIWritablePropertyBag2
  1076. // doesn't inherit from that, so the additional QI is needed.
  1077. let formInfo = Cc["@mozilla.org/hash-property-bag;1"].
  1078. createInstance(Ci.nsIWritablePropertyBag2).
  1079. QueryInterface(Ci.nsIWritablePropertyBag);
  1080. formInfo.setPropertyAsACString("didntFillReason", didntFillReason);
  1081. formInfo.setPropertyAs

Large files files are truncated, but you can click here to view the full file