toolkit/modules/sessionstore/FormData.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/modules/sessionstore/FormData.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,364 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +this.EXPORTED_SYMBOLS = ["FormData"];
    1.11 +
    1.12 +const Cu = Components.utils;
    1.13 +const Ci = Components.interfaces;
    1.14 +
    1.15 +Cu.import("resource://gre/modules/Timer.jsm");
    1.16 +Cu.import("resource://gre/modules/XPathGenerator.jsm");
    1.17 +
    1.18 +/**
    1.19 + * Returns whether the given URL very likely has input
    1.20 + * fields that contain serialized session store data.
    1.21 + */
    1.22 +function isRestorationPage(url) {
    1.23 +  return url == "about:sessionrestore" || url == "about:welcomeback";
    1.24 +}
    1.25 +
    1.26 +/**
    1.27 + * Returns whether the given form |data| object contains nested restoration
    1.28 + * data for a page like about:sessionrestore or about:welcomeback.
    1.29 + */
    1.30 +function hasRestorationData(data) {
    1.31 +  if (isRestorationPage(data.url) && data.id) {
    1.32 +    return typeof(data.id.sessionData) == "object";
    1.33 +  }
    1.34 +
    1.35 +  return false;
    1.36 +}
    1.37 +
    1.38 +/**
    1.39 + * Returns the given document's current URI and strips
    1.40 + * off the URI's anchor part, if any.
    1.41 + */
    1.42 +function getDocumentURI(doc) {
    1.43 +  return doc.documentURI.replace(/#.*$/, "");
    1.44 +}
    1.45 +
    1.46 +/**
    1.47 + * The public API exported by this module that allows to collect
    1.48 + * and restore form data for a document and its subframes.
    1.49 + */
    1.50 +this.FormData = Object.freeze({
    1.51 +  collect: function (frame) {
    1.52 +    return FormDataInternal.collect(frame);
    1.53 +  },
    1.54 +
    1.55 +  restore: function (frame, data) {
    1.56 +    FormDataInternal.restore(frame, data);
    1.57 +  },
    1.58 +
    1.59 +  restoreTree: function (root, data) {
    1.60 +    FormDataInternal.restoreTree(root, data);
    1.61 +  }
    1.62 +});
    1.63 +
    1.64 +/**
    1.65 + * This module's internal API.
    1.66 + */
    1.67 +let FormDataInternal = {
    1.68 +  /**
    1.69 +   * Collect form data for a given |frame| *not* including any subframes.
    1.70 +   *
    1.71 +   * The returned object may have an "id", "xpath", or "innerHTML" key or a
    1.72 +   * combination of those three. Form data stored under "id" is for input
    1.73 +   * fields with id attributes. Data stored under "xpath" is used for input
    1.74 +   * fields that don't have a unique id and need to be queried using XPath.
    1.75 +   * The "innerHTML" key is used for editable documents (designMode=on).
    1.76 +   *
    1.77 +   * Example:
    1.78 +   *   {
    1.79 +   *     id: {input1: "value1", input3: "value3"},
    1.80 +   *     xpath: {
    1.81 +   *       "/xhtml:html/xhtml:body/xhtml:input[@name='input2']" : "value2",
    1.82 +   *       "/xhtml:html/xhtml:body/xhtml:input[@name='input4']" : "value4"
    1.83 +   *     }
    1.84 +   *   }
    1.85 +   *
    1.86 +   * @param  doc
    1.87 +   *         DOMDocument instance to obtain form data for.
    1.88 +   * @return object
    1.89 +   *         Form data encoded in an object.
    1.90 +   */
    1.91 +  collect: function ({document: doc}) {
    1.92 +    let formNodes = doc.evaluate(
    1.93 +      XPathGenerator.restorableFormNodes,
    1.94 +      doc,
    1.95 +      XPathGenerator.resolveNS,
    1.96 +      Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE, null
    1.97 +    );
    1.98 +
    1.99 +    let node;
   1.100 +    let ret = {};
   1.101 +
   1.102 +    // Limit the number of XPath expressions for performance reasons. See
   1.103 +    // bug 477564.
   1.104 +    const MAX_TRAVERSED_XPATHS = 100;
   1.105 +    let generatedCount = 0;
   1.106 +
   1.107 +    while (node = formNodes.iterateNext()) {
   1.108 +      let hasDefaultValue = true;
   1.109 +      let value;
   1.110 +
   1.111 +      // Only generate a limited number of XPath expressions for perf reasons
   1.112 +      // (cf. bug 477564)
   1.113 +      if (!node.id && generatedCount > MAX_TRAVERSED_XPATHS) {
   1.114 +        continue;
   1.115 +      }
   1.116 +
   1.117 +      if (node instanceof Ci.nsIDOMHTMLInputElement ||
   1.118 +          node instanceof Ci.nsIDOMHTMLTextAreaElement ||
   1.119 +          node instanceof Ci.nsIDOMXULTextBoxElement) {
   1.120 +        switch (node.type) {
   1.121 +          case "checkbox":
   1.122 +          case "radio":
   1.123 +            value = node.checked;
   1.124 +            hasDefaultValue = value == node.defaultChecked;
   1.125 +            break;
   1.126 +          case "file":
   1.127 +            value = { type: "file", fileList: node.mozGetFileNameArray() };
   1.128 +            hasDefaultValue = !value.fileList.length;
   1.129 +            break;
   1.130 +          default: // text, textarea
   1.131 +            value = node.value;
   1.132 +            hasDefaultValue = value == node.defaultValue;
   1.133 +            break;
   1.134 +        }
   1.135 +      } else if (!node.multiple) {
   1.136 +        // <select>s without the multiple attribute are hard to determine the
   1.137 +        // default value, so assume we don't have the default.
   1.138 +        hasDefaultValue = false;
   1.139 +        value = { selectedIndex: node.selectedIndex, value: node.value };
   1.140 +      } else {
   1.141 +        // <select>s with the multiple attribute are easier to determine the
   1.142 +        // default value since each <option> has a defaultSelected property
   1.143 +        let options = Array.map(node.options, opt => {
   1.144 +          hasDefaultValue = hasDefaultValue && (opt.selected == opt.defaultSelected);
   1.145 +          return opt.selected ? opt.value : -1;
   1.146 +        });
   1.147 +        value = options.filter(ix => ix > -1);
   1.148 +      }
   1.149 +
   1.150 +      // In order to reduce XPath generation (which is slow), we only save data
   1.151 +      // for form fields that have been changed. (cf. bug 537289)
   1.152 +      if (hasDefaultValue) {
   1.153 +        continue;
   1.154 +      }
   1.155 +
   1.156 +      if (node.id) {
   1.157 +        ret.id = ret.id || {};
   1.158 +        ret.id[node.id] = value;
   1.159 +      } else {
   1.160 +        generatedCount++;
   1.161 +        ret.xpath = ret.xpath || {};
   1.162 +        ret.xpath[XPathGenerator.generate(node)] = value;
   1.163 +      }
   1.164 +    }
   1.165 +
   1.166 +    // designMode is undefined e.g. for XUL documents (as about:config)
   1.167 +    if ((doc.designMode || "") == "on" && doc.body) {
   1.168 +      ret.innerHTML = doc.body.innerHTML;
   1.169 +    }
   1.170 +
   1.171 +    // Return |null| if no form data has been found.
   1.172 +    if (Object.keys(ret).length === 0) {
   1.173 +      return null;
   1.174 +    }
   1.175 +
   1.176 +    // Store the frame's current URL with its form data so that we can compare
   1.177 +    // it when restoring data to not inject form data into the wrong document.
   1.178 +    ret.url = getDocumentURI(doc);
   1.179 +
   1.180 +    // We want to avoid saving data for about:sessionrestore as a string.
   1.181 +    // Since it's stored in the form as stringified JSON, stringifying further
   1.182 +    // causes an explosion of escape characters. cf. bug 467409
   1.183 +    if (isRestorationPage(ret.url)) {
   1.184 +      ret.id.sessionData = JSON.parse(ret.id.sessionData);
   1.185 +    }
   1.186 +
   1.187 +    return ret;
   1.188 +  },
   1.189 +
   1.190 +  /**
   1.191 +   * Restores form |data| for the given frame. The data is expected to be in
   1.192 +   * the same format that FormData.collect() returns.
   1.193 +   *
   1.194 +   * @param frame (DOMWindow)
   1.195 +   *        The frame to restore form data to.
   1.196 +   * @param data (object)
   1.197 +   *        An object holding form data.
   1.198 +   */
   1.199 +  restore: function ({document: doc}, data) {
   1.200 +    // Don't restore any data for the given frame if the URL
   1.201 +    // stored in the form data doesn't match its current URL.
   1.202 +    if (!data.url || data.url != getDocumentURI(doc)) {
   1.203 +      return;
   1.204 +    }
   1.205 +
   1.206 +    // For about:{sessionrestore,welcomeback} we saved the field as JSON to
   1.207 +    // avoid nested instances causing humongous sessionstore.js files.
   1.208 +    // cf. bug 467409
   1.209 +    if (hasRestorationData(data)) {
   1.210 +      data.id.sessionData = JSON.stringify(data.id.sessionData);
   1.211 +    }
   1.212 +
   1.213 +    if ("id" in data) {
   1.214 +      let retrieveNode = id => doc.getElementById(id);
   1.215 +      this.restoreManyInputValues(data.id, retrieveNode);
   1.216 +    }
   1.217 +
   1.218 +    if ("xpath" in data) {
   1.219 +      let retrieveNode = xpath => XPathGenerator.resolve(doc, xpath);
   1.220 +      this.restoreManyInputValues(data.xpath, retrieveNode);
   1.221 +    }
   1.222 +
   1.223 +    if ("innerHTML" in data) {
   1.224 +      // We know that the URL matches data.url right now, but the user
   1.225 +      // may navigate away before the setTimeout handler runs. We do
   1.226 +      // a simple comparison against savedURL to check for that.
   1.227 +      let savedURL = doc.documentURI;
   1.228 +
   1.229 +      setTimeout(() => {
   1.230 +        if (doc.body && doc.designMode == "on" && doc.documentURI == savedURL) {
   1.231 +          doc.body.innerHTML = data.innerHTML;
   1.232 +        }
   1.233 +      });
   1.234 +    }
   1.235 +  },
   1.236 +
   1.237 +  /**
   1.238 +   * Iterates the given form data, retrieving nodes for all the keys and
   1.239 +   * restores their appropriate values.
   1.240 +   *
   1.241 +   * @param data (object)
   1.242 +   *        A subset of the form data as collected by FormData.collect(). This
   1.243 +   *        is either data stored under "id" or under "xpath".
   1.244 +   * @param retrieve (function)
   1.245 +   *        The function used to retrieve the input field belonging to a key
   1.246 +   *        in the given |data| object.
   1.247 +   */
   1.248 +  restoreManyInputValues: function (data, retrieve) {
   1.249 +    for (let key of Object.keys(data)) {
   1.250 +      let input = retrieve(key);
   1.251 +      if (input) {
   1.252 +        this.restoreSingleInputValue(input, data[key]);
   1.253 +      }
   1.254 +    }
   1.255 +  },
   1.256 +
   1.257 +  /**
   1.258 +   * Restores a given form value to a given DOMNode and takes care of firing
   1.259 +   * the appropriate DOM event should the input's value change.
   1.260 +   *
   1.261 +   * @param  aNode
   1.262 +   *         DOMNode to set form value on.
   1.263 +   * @param  aValue
   1.264 +   *         Value to set form element to.
   1.265 +   */
   1.266 +  restoreSingleInputValue: function (aNode, aValue) {
   1.267 +    let eventType;
   1.268 +
   1.269 +    if (typeof aValue == "string" && aNode.type != "file") {
   1.270 +      // Don't dispatch an input event if there is no change.
   1.271 +      if (aNode.value == aValue) {
   1.272 +        return;
   1.273 +      }
   1.274 +
   1.275 +      aNode.value = aValue;
   1.276 +      eventType = "input";
   1.277 +    } else if (typeof aValue == "boolean") {
   1.278 +      // Don't dispatch a change event for no change.
   1.279 +      if (aNode.checked == aValue) {
   1.280 +        return;
   1.281 +      }
   1.282 +
   1.283 +      aNode.checked = aValue;
   1.284 +      eventType = "change";
   1.285 +    } else if (aValue && aValue.selectedIndex >= 0 && aValue.value) {
   1.286 +      // Don't dispatch a change event for no change
   1.287 +      if (aNode.options[aNode.selectedIndex].value == aValue.value) {
   1.288 +        return;
   1.289 +      }
   1.290 +
   1.291 +      // find first option with matching aValue if possible
   1.292 +      for (let i = 0; i < aNode.options.length; i++) {
   1.293 +        if (aNode.options[i].value == aValue.value) {
   1.294 +          aNode.selectedIndex = i;
   1.295 +          eventType = "change";
   1.296 +          break;
   1.297 +        }
   1.298 +      }
   1.299 +    } else if (aValue && aValue.fileList && aValue.type == "file" &&
   1.300 +      aNode.type == "file") {
   1.301 +      aNode.mozSetFileNameArray(aValue.fileList, aValue.fileList.length);
   1.302 +      eventType = "input";
   1.303 +    } else if (Array.isArray(aValue) && aNode.options) {
   1.304 +      Array.forEach(aNode.options, function(opt, index) {
   1.305 +        // don't worry about malformed options with same values
   1.306 +        opt.selected = aValue.indexOf(opt.value) > -1;
   1.307 +
   1.308 +        // Only fire the event here if this wasn't selected by default
   1.309 +        if (!opt.defaultSelected) {
   1.310 +          eventType = "change";
   1.311 +        }
   1.312 +      });
   1.313 +    }
   1.314 +
   1.315 +    // Fire events for this node if applicable
   1.316 +    if (eventType) {
   1.317 +      let doc = aNode.ownerDocument;
   1.318 +      let event = doc.createEvent("UIEvents");
   1.319 +      event.initUIEvent(eventType, true, true, doc.defaultView, 0);
   1.320 +      aNode.dispatchEvent(event);
   1.321 +    }
   1.322 +  },
   1.323 +
   1.324 +  /**
   1.325 +   * Restores form data for the current frame hierarchy starting at |root|
   1.326 +   * using the given form |data|.
   1.327 +   *
   1.328 +   * If the given |root| frame's hierarchy doesn't match that of the given
   1.329 +   * |data| object we will silently discard data for unreachable frames. For
   1.330 +   * security reasons we will never restore form data to the wrong frames as
   1.331 +   * we bail out silently if the stored URL doesn't match the frame's current
   1.332 +   * URL.
   1.333 +   *
   1.334 +   * @param root (DOMWindow)
   1.335 +   * @param data (object)
   1.336 +   *        {
   1.337 +   *          formdata: {id: {input1: "value1"}},
   1.338 +   *          children: [
   1.339 +   *            {formdata: {id: {input2: "value2"}}},
   1.340 +   *            null,
   1.341 +   *            {formdata: {xpath: { ... }}, children: [ ... ]}
   1.342 +   *          ]
   1.343 +   *        }
   1.344 +   */
   1.345 +  restoreTree: function (root, data) {
   1.346 +    // Don't restore any data for the root frame and its subframes if there
   1.347 +    // is a URL stored in the form data and it doesn't match its current URL.
   1.348 +    if (data.url && data.url != getDocumentURI(root.document)) {
   1.349 +      return;
   1.350 +    }
   1.351 +
   1.352 +    if (data.url) {
   1.353 +      this.restore(root, data);
   1.354 +    }
   1.355 +
   1.356 +    if (!data.hasOwnProperty("children")) {
   1.357 +      return;
   1.358 +    }
   1.359 +
   1.360 +    let frames = root.frames;
   1.361 +    for (let index of Object.keys(data.children)) {
   1.362 +      if (index < frames.length) {
   1.363 +        this.restoreTree(frames[index], data.children[index]);
   1.364 +      }
   1.365 +    }
   1.366 +  }
   1.367 +};

mercurial