Path: blob/trunk/third_party/closure/goog/net/iframeio.js
2868 views
// Copyright 2006 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview Class for managing requests via iFrames. Supports a number of16* methods of transfer.17*18* Gets and Posts can be performed and the resultant page read in as text,19* JSON, or from the HTML DOM.20*21* Using an iframe causes the throbber to spin, this is good for providing22* feedback to the user that an action has occurred.23*24* Requests do not affect the history stack, see goog.History if you require25* this behavior.26*27* The responseText and responseJson methods assume the response is plain,28* text. You can access the Iframe's DOM through responseXml if you need29* access to the raw HTML.30*31* Tested:32* + FF2.0 (Win Linux)33* + IE6, IE734* + Opera 9.1,35* + Chrome36* - Opera 8.5 fails because of no textContent and buggy innerText support37*38* NOTE: Safari doesn't fire the onload handler when loading plain text files39*40* This has been tested with Drip in IE to ensure memory usage is as constant41* as possible. When making making thousands of requests, memory usage stays42* constant for a while but then starts increasing (<500k for 200043* requests) -- this hasn't yet been tracked down yet, though it is cleared up44* after a refresh.45*46*47* BACKGROUND FILE UPLOAD:48* By posting an arbitrary form through an IframeIo object, it is possible to49* implement background file uploads. Here's how to do it:50*51* - Create a form:52* <pre>53* <form id="form" enctype="multipart/form-data" method="POST">54* <input name="userfile" type="file" />55* </form>56* </pre>57*58* - Have the user click the file input59* - Create an IframeIo instance60* <pre>61* var io = new goog.net.IframeIo;62* goog.events.listen(io, goog.net.EventType.COMPLETE,63* function() { alert('Sent'); });64* io.sendFromForm(document.getElementById('form'));65* </pre>66*67*68* INCREMENTAL LOADING:69* Gmail sends down multiple script blocks which get executed as they are70* received by the client. This allows incremental rendering of the thread71* list and conversations.72*73* This requires collaboration with the server that is sending the requested74* page back. To set incremental loading up, you should:75*76* A) In the application code there should be an externed reference to77* <code>handleIncrementalData()</code>. e.g.78* goog.exportSymbol('GG_iframeFn', goog.net.IframeIo.handleIncrementalData);79*80* B) The response page should them call this method directly, an example81* response would look something like this:82* <pre>83* <html>84* <head>85* <meta content="text/html;charset=UTF-8" http-equiv="content-type">86* </head>87* <body>88* <script>89* D = top.P ? function(d) { top.GG_iframeFn(window, d) } : function() {};90* </script>91*92* <script>D([1, 2, 3, 4, 5]);</script>93* <script>D([6, 7, 8, 9, 10]);</script>94* <script>D([11, 12, 13, 14, 15]);</script>95* </body>96* </html>97* </pre>98*99* Your application should then listen, on the IframeIo instance, to the event100* goog.net.EventType.INCREMENTAL_DATA. The event object contains a101* 'data' member which is the content from the D() calls above.102*103* NOTE: There can be problems if you save a reference to the data object in IE.104* If you save an array, and the iframe is dispose, then the array looses its105* prototype and thus array methods like .join(). You can get around this by106* creating arrays using the parent window's Array constructor, or you can107* clone the array.108*109*110* EVENT MODEL:111* The various send methods work asynchronously. You can be notified about112* the current status of the request (completed, success or error) by113* listening for events on the IframeIo object itself. The following events114* will be sent:115* - goog.net.EventType.COMPLETE: when the request is completed116* (either successfully or unsuccessfully). You can find out about the result117* using the isSuccess() and getLastError118* methods.119* - goog.net.EventType.SUCCESS</code>: when the request was completed120* successfully121* - goog.net.EventType.ERROR: when the request failed122* - goog.net.EventType.ABORT: when the request has been aborted123*124* Example:125* <pre>126* var io = new goog.net.IframeIo();127* goog.events.listen(io, goog.net.EventType.COMPLETE,128* function() { alert('request complete'); });129* io.sendFromForm(...);130* </pre>131*132*/133134goog.provide('goog.net.IframeIo');135goog.provide('goog.net.IframeIo.IncrementalDataEvent');136137goog.require('goog.Timer');138goog.require('goog.Uri');139goog.require('goog.array');140goog.require('goog.asserts');141goog.require('goog.debug.HtmlFormatter');142goog.require('goog.dom');143goog.require('goog.dom.InputType');144goog.require('goog.dom.TagName');145goog.require('goog.dom.safe');146goog.require('goog.events');147goog.require('goog.events.Event');148goog.require('goog.events.EventTarget');149goog.require('goog.events.EventType');150goog.require('goog.html.uncheckedconversions');151goog.require('goog.json');152goog.require('goog.log');153goog.require('goog.log.Level');154goog.require('goog.net.ErrorCode');155goog.require('goog.net.EventType');156goog.require('goog.reflect');157goog.require('goog.string');158goog.require('goog.string.Const');159goog.require('goog.structs');160goog.require('goog.userAgent');161162163164/**165* Class for managing requests via iFrames.166* @constructor167* @extends {goog.events.EventTarget}168*/169goog.net.IframeIo = function() {170goog.net.IframeIo.base(this, 'constructor');171172/**173* Name for this IframeIo and frame174* @type {string}175* @private176*/177this.name_ = goog.net.IframeIo.getNextName_();178179/**180* An array of iframes that have been finished with. We need them to be181* disposed async, so we don't confuse the browser (see below).182* @type {Array<Element>}183* @private184*/185this.iframesForDisposal_ = [];186187// Create a lookup from names to instances of IframeIo. This is a helper188// function to be used in conjunction with goog.net.IframeIo.getInstanceByName189// to find the IframeIo object associated with a particular iframe. Used in190// incremental scripts etc.191goog.net.IframeIo.instances_[this.name_] = this;192193};194goog.inherits(goog.net.IframeIo, goog.events.EventTarget);195196197/**198* Object used as a map to lookup instances of IframeIo objects by name.199* @type {Object}200* @private201*/202goog.net.IframeIo.instances_ = {};203204205/**206* Prefix for frame names207* @type {string}208*/209goog.net.IframeIo.FRAME_NAME_PREFIX = 'closure_frame';210211212/**213* Suffix that is added to inner frames used for sending requests in non-IE214* browsers215* @type {string}216*/217goog.net.IframeIo.INNER_FRAME_SUFFIX = '_inner';218219220/**221* The number of milliseconds after a request is completed to dispose the222* iframes. This can be done lazily so we wait long enough for any processing223* that occurred as a result of the response to finish.224* @type {number}225*/226goog.net.IframeIo.IFRAME_DISPOSE_DELAY_MS = 2000;227228229/**230* Counter used when creating iframes231* @type {number}232* @private233*/234goog.net.IframeIo.counter_ = 0;235236237/**238* Form element to post to.239* @type {HTMLFormElement}240* @private241*/242goog.net.IframeIo.form_;243244245/**246* Static send that creates a short lived instance of IframeIo to send the247* request.248* @param {goog.Uri|string} uri Uri of the request, it is up the caller to249* manage query string params.250* @param {Function=} opt_callback Event handler for when request is completed.251* @param {string=} opt_method Default is GET, POST uses a form to submit the252* request.253* @param {boolean=} opt_noCache Append a timestamp to the request to avoid254* caching.255* @param {Object|goog.structs.Map=} opt_data Map of key-value pairs that256* will be posted to the server via the iframe's form.257*/258goog.net.IframeIo.send = function(259uri, opt_callback, opt_method, opt_noCache, opt_data) {260261var io = new goog.net.IframeIo();262goog.events.listen(io, goog.net.EventType.READY, io.dispose, false, io);263if (opt_callback) {264goog.events.listen(io, goog.net.EventType.COMPLETE, opt_callback);265}266io.send(uri, opt_method, opt_noCache, opt_data);267};268269270/**271* Find an iframe by name (assumes the context is goog.global since that is272* where IframeIo's iframes are kept).273* @param {string} fname The name to find.274* @return {HTMLIFrameElement} The iframe element with that name.275*/276goog.net.IframeIo.getIframeByName = function(fname) {277return window.frames[fname];278};279280281/**282* Find an instance of the IframeIo object by name.283* @param {string} fname The name to find.284* @return {goog.net.IframeIo} The instance of IframeIo.285*/286goog.net.IframeIo.getInstanceByName = function(fname) {287return goog.net.IframeIo.instances_[fname];288};289290291/**292* Handles incremental data and routes it to the correct iframeIo instance.293* The HTML page requested by the IframeIo instance should contain script blocks294* that call an externed reference to this method.295* @param {Window} win The window object.296* @param {Object} data The data object.297*/298goog.net.IframeIo.handleIncrementalData = function(win, data) {299// If this is the inner-frame, then we need to use the parent instead.300var iframeName =301goog.string.endsWith(win.name, goog.net.IframeIo.INNER_FRAME_SUFFIX) ?302win.parent.name :303win.name;304305var iframeIoName = iframeName.substring(0, iframeName.lastIndexOf('_'));306var iframeIo = goog.net.IframeIo.getInstanceByName(iframeIoName);307if (iframeIo && iframeName == iframeIo.iframeName_) {308iframeIo.handleIncrementalData_(data);309} else {310var logger = goog.log.getLogger('goog.net.IframeIo');311goog.log.info(logger, 'Incremental iframe data routed for unknown iframe');312}313};314315316/**317* @return {string} The next iframe name.318* @private319*/320goog.net.IframeIo.getNextName_ = function() {321return goog.net.IframeIo.FRAME_NAME_PREFIX + goog.net.IframeIo.counter_++;322};323324325/**326* Gets a static form, one for all instances of IframeIo since IE6 leaks form327* nodes that are created/removed from the document.328* @return {!HTMLFormElement} The static form.329* @private330*/331goog.net.IframeIo.getForm_ = function() {332if (!goog.net.IframeIo.form_) {333goog.net.IframeIo.form_ = goog.dom.createDom(goog.dom.TagName.FORM);334goog.net.IframeIo.form_.acceptCharset = 'utf-8';335336// Hide the form and move it off screen337var s = goog.net.IframeIo.form_.style;338s.position = 'absolute';339s.visibility = 'hidden';340s.top = s.left = '-10px';341s.width = s.height = '10px';342s.overflow = 'hidden';343344goog.dom.getDocument().body.appendChild(goog.net.IframeIo.form_);345}346return goog.net.IframeIo.form_;347};348349350/**351* Adds the key value pairs from a map like data structure to a form352* @param {HTMLFormElement} form The form to add to.353* @param {Object|goog.structs.Map|goog.Uri.QueryData} data The data to add.354* @private355*/356goog.net.IframeIo.addFormInputs_ = function(form, data) {357var helper = goog.dom.getDomHelper(form);358goog.structs.forEach(data, function(value, key) {359if (!goog.isArray(value)) {360value = [value];361}362goog.array.forEach(value, function(value) {363var inp = helper.createDom(364goog.dom.TagName.INPUT,365{'type': goog.dom.InputType.HIDDEN, 'name': key, 'value': value});366form.appendChild(inp);367});368});369};370371372/**373* @return {boolean} Whether we can use readyState to monitor iframe loading.374* @private375*/376goog.net.IframeIo.useIeReadyStateCodePath_ = function() {377// ReadyState is only available on iframes up to IE10.378return goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('11');379};380381382/**383* Reference to a logger for the IframeIo objects384* @type {goog.log.Logger}385* @private386*/387goog.net.IframeIo.prototype.logger_ = goog.log.getLogger('goog.net.IframeIo');388389390/**391* Reference to form element that gets reused for requests to the iframe.392* @type {HTMLFormElement}393* @private394*/395goog.net.IframeIo.prototype.form_ = null;396397398/**399* Reference to the iframe being used for the current request, or null if no400* request is currently active.401* @type {HTMLIFrameElement}402* @private403*/404goog.net.IframeIo.prototype.iframe_ = null;405406407/**408* Name of the iframe being used for the current request, or null if no409* request is currently active.410* @type {?string}411* @private412*/413goog.net.IframeIo.prototype.iframeName_ = null;414415416/**417* Next id so that iframe names are unique.418* @type {number}419* @private420*/421goog.net.IframeIo.prototype.nextIframeId_ = 0;422423424/**425* Whether the object is currently active with a request.426* @type {boolean}427* @private428*/429goog.net.IframeIo.prototype.active_ = false;430431432/**433* Whether the last request is complete.434* @type {boolean}435* @private436*/437goog.net.IframeIo.prototype.complete_ = false;438439440/**441* Whether the last request was a success.442* @type {boolean}443* @private444*/445goog.net.IframeIo.prototype.success_ = false;446447448/**449* The URI for the last request.450* @type {goog.Uri}451* @private452*/453goog.net.IframeIo.prototype.lastUri_ = null;454455456/**457* The text content of the last request.458* @type {?string}459* @private460*/461goog.net.IframeIo.prototype.lastContent_ = null;462463464/**465* Last error code466* @type {goog.net.ErrorCode}467* @private468*/469goog.net.IframeIo.prototype.lastErrorCode_ = goog.net.ErrorCode.NO_ERROR;470471472/**473* Window timeout ID used to detect when firefox silently fails.474* @type {?number}475* @private476*/477goog.net.IframeIo.prototype.firefoxSilentErrorTimeout_ = null;478479480/**481* Window timeout ID used by the timer that disposes the iframes.482* @type {?number}483* @private484*/485goog.net.IframeIo.prototype.iframeDisposalTimer_ = null;486487488/**489* This is used to ensure that we don't handle errors twice for the same error.490* We can reach the {@link #handleError_} method twice in IE if the form is491* submitted while IE is offline and the URL is not available.492* @type {boolean}493* @private494*/495goog.net.IframeIo.prototype.errorHandled_;496497498/**499* Whether to suppress the listeners that determine when the iframe loads.500* @type {boolean}501* @private502*/503goog.net.IframeIo.prototype.ignoreResponse_ = false;504505506/** @private {Function} */507goog.net.IframeIo.prototype.errorChecker_;508509510/** @private {Object} */511goog.net.IframeIo.prototype.lastCustomError_;512513514/** @private {?string} */515goog.net.IframeIo.prototype.lastContentHtml_;516517518/**519* Sends a request via an iframe.520*521* A HTML form is used and submitted to the iframe, this simplifies the522* difference between GET and POST requests. The iframe needs to be created and523* destroyed for each request otherwise the request will contribute to the524* history stack.525*526* sendFromForm does some clever trickery (thanks jlim) in non-IE browsers to527* stop a history entry being added for POST requests.528*529* @param {goog.Uri|string} uri Uri of the request.530* @param {string=} opt_method Default is GET, POST uses a form to submit the531* request.532* @param {boolean=} opt_noCache Append a timestamp to the request to avoid533* caching.534* @param {Object|goog.structs.Map=} opt_data Map of key-value pairs.535*/536goog.net.IframeIo.prototype.send = function(537uri, opt_method, opt_noCache, opt_data) {538539if (this.active_) {540throw Error('[goog.net.IframeIo] Unable to send, already active.');541}542543var uriObj = new goog.Uri(uri);544this.lastUri_ = uriObj;545var method = opt_method ? opt_method.toUpperCase() : 'GET';546547if (opt_noCache) {548uriObj.makeUnique();549}550551goog.log.info(552this.logger_, 'Sending iframe request: ' + uriObj + ' [' + method + ']');553554// Build a form for this request555this.form_ = goog.net.IframeIo.getForm_();556557if (method == 'GET') {558// For GET requests, we assume that the caller didn't want the queryparams559// already specified in the URI to be clobbered by the form, so we add the560// params here.561goog.net.IframeIo.addFormInputs_(this.form_, uriObj.getQueryData());562}563564if (opt_data) {565// Create form fields for each of the data values566goog.net.IframeIo.addFormInputs_(this.form_, opt_data);567}568569// Set the URI that the form will be posted570this.form_.action = uriObj.toString();571this.form_.method = method;572573this.sendFormInternal_();574this.clearForm_();575};576577578/**579* Sends the data stored in an existing form to the server. The HTTP method580* should be specified on the form, the action can also be specified but can581* be overridden by the optional URI param.582*583* This can be used in conjunction will a file-upload input to upload a file in584* the background without affecting history.585*586* Example form:587* <pre>588* <form action="/server/" enctype="multipart/form-data" method="POST">589* <input name="userfile" type="file">590* </form>591* </pre>592*593* @param {HTMLFormElement} form Form element used to send the request to the594* server.595* @param {string=} opt_uri Uri to set for the destination of the request, by596* default the uri will come from the form.597* @param {boolean=} opt_noCache Append a timestamp to the request to avoid598* caching.599*/600goog.net.IframeIo.prototype.sendFromForm = function(601form, opt_uri, opt_noCache) {602if (this.active_) {603throw Error('[goog.net.IframeIo] Unable to send, already active.');604}605606var uri = new goog.Uri(opt_uri || form.action);607if (opt_noCache) {608uri.makeUnique();609}610611goog.log.info(this.logger_, 'Sending iframe request from form: ' + uri);612613this.lastUri_ = uri;614this.form_ = form;615this.form_.action = uri.toString();616this.sendFormInternal_();617};618619620/**621* Abort the current Iframe request622* @param {goog.net.ErrorCode=} opt_failureCode Optional error code to use -623* defaults to ABORT.624*/625goog.net.IframeIo.prototype.abort = function(opt_failureCode) {626if (this.active_) {627goog.log.info(this.logger_, 'Request aborted');628var requestIframe = this.getRequestIframe();629goog.asserts.assert(requestIframe);630goog.events.removeAll(requestIframe);631this.complete_ = false;632this.active_ = false;633this.success_ = false;634this.lastErrorCode_ = opt_failureCode || goog.net.ErrorCode.ABORT;635636this.dispatchEvent(goog.net.EventType.ABORT);637638this.makeReady_();639}640};641642643/** @override */644goog.net.IframeIo.prototype.disposeInternal = function() {645goog.log.fine(this.logger_, 'Disposing iframeIo instance');646647// If there is an active request, abort it648if (this.active_) {649goog.log.fine(this.logger_, 'Aborting active request');650this.abort();651}652653// Call super-classes implementation (remove listeners)654goog.net.IframeIo.superClass_.disposeInternal.call(this);655656// Add the current iframe to the list of iframes for disposal.657if (this.iframe_) {658this.scheduleIframeDisposal_();659}660661// Disposes of the form662this.disposeForm_();663664// Nullify anything that might cause problems and clear state665delete this.errorChecker_;666this.form_ = null;667this.lastCustomError_ = this.lastContent_ = this.lastContentHtml_ = null;668this.lastUri_ = null;669this.lastErrorCode_ = goog.net.ErrorCode.NO_ERROR;670671delete goog.net.IframeIo.instances_[this.name_];672};673674675/**676* @return {boolean} True if transfer is complete.677*/678goog.net.IframeIo.prototype.isComplete = function() {679return this.complete_;680};681682683/**684* @return {boolean} True if transfer was successful.685*/686goog.net.IframeIo.prototype.isSuccess = function() {687return this.success_;688};689690691/**692* @return {boolean} True if a transfer is in progress.693*/694goog.net.IframeIo.prototype.isActive = function() {695return this.active_;696};697698699/**700* Returns the last response text (i.e. the text content of the iframe).701* Assumes plain text!702* @return {?string} Result from the server.703*/704goog.net.IframeIo.prototype.getResponseText = function() {705return this.lastContent_;706};707708709/**710* Returns the last response html (i.e. the innerHtml of the iframe).711* @return {?string} Result from the server.712*/713goog.net.IframeIo.prototype.getResponseHtml = function() {714return this.lastContentHtml_;715};716717718/**719* Parses the content as JSON. This is a legacy method for browsers without720* JSON.parse or for responses that are not valid JSON (e.g. containing NaN).721* Use JSON.parse(this.getResponseText()) in the other cases.722* @return {Object} The parsed content.723*/724goog.net.IframeIo.prototype.getResponseJson = function() {725return goog.json.parse(this.lastContent_);726};727728729/**730* Returns the document object from the last request. Not truly XML, but731* used to mirror the XhrIo interface.732* @return {HTMLDocument} The document object from the last request.733*/734goog.net.IframeIo.prototype.getResponseXml = function() {735if (!this.iframe_) return null;736737return this.getContentDocument_();738};739740741/**742* Get the uri of the last request.743* @return {goog.Uri} Uri of last request.744*/745goog.net.IframeIo.prototype.getLastUri = function() {746return this.lastUri_;747};748749750/**751* Gets the last error code.752* @return {goog.net.ErrorCode} Last error code.753*/754goog.net.IframeIo.prototype.getLastErrorCode = function() {755return this.lastErrorCode_;756};757758759/**760* Gets the last error message.761* @return {string} Last error message.762*/763goog.net.IframeIo.prototype.getLastError = function() {764return goog.net.ErrorCode.getDebugMessage(this.lastErrorCode_);765};766767768/**769* Gets the last custom error.770* @return {Object} Last custom error.771*/772goog.net.IframeIo.prototype.getLastCustomError = function() {773return this.lastCustomError_;774};775776777/**778* Sets the callback function used to check if a loaded IFrame is in an error779* state.780* @param {Function} fn Callback that expects a document object as it's single781* argument.782*/783goog.net.IframeIo.prototype.setErrorChecker = function(fn) {784this.errorChecker_ = fn;785};786787788/**789* Gets the callback function used to check if a loaded IFrame is in an error790* state.791* @return {Function} A callback that expects a document object as it's single792* argument.793*/794goog.net.IframeIo.prototype.getErrorChecker = function() {795return this.errorChecker_;796};797798799/**800* @return {boolean} Whether the server response is being ignored.801*/802goog.net.IframeIo.prototype.isIgnoringResponse = function() {803return this.ignoreResponse_;804};805806807/**808* Sets whether to ignore the response from the server by not adding any event809* handlers to fire when the iframe loads. This is necessary when using IframeIo810* to submit to a server on another domain, to avoid same-origin violations when811* trying to access the response. If this is set to true, the IframeIo instance812* will be a single-use instance that is only usable for one request. It will813* only clean up its resources (iframes and forms) when it is disposed.814* @param {boolean} ignore Whether to ignore the server response.815*/816goog.net.IframeIo.prototype.setIgnoreResponse = function(ignore) {817this.ignoreResponse_ = ignore;818};819820821/**822* Submits the internal form to the iframe.823* @private824*/825goog.net.IframeIo.prototype.sendFormInternal_ = function() {826this.active_ = true;827this.complete_ = false;828this.lastErrorCode_ = goog.net.ErrorCode.NO_ERROR;829830// Make Iframe831this.createIframe_();832833if (goog.net.IframeIo.useIeReadyStateCodePath_()) {834// In IE<11 we simply create the frame, wait until it is ready, then post835// the form to the iframe and wait for the readystate to change to836// 'complete'837838// Set the target to the iframe's name839this.form_.target = this.iframeName_ || '';840this.appendIframe_();841if (!this.ignoreResponse_) {842goog.events.listen(843this.iframe_, goog.events.EventType.READYSTATECHANGE,844this.onIeReadyStateChange_, false, this);845}846847848try {849this.errorHandled_ = false;850this.form_.submit();851} catch (e) {852// If submit threw an exception then it probably means the page that the853// code is running on the local file system and the form's action was854// pointing to a file that doesn't exist, causing the browser to fire an855// exception. IE also throws an exception when it is working offline and856// the URL is not available.857858if (!this.ignoreResponse_) {859goog.events.unlisten(860this.iframe_, goog.events.EventType.READYSTATECHANGE,861this.onIeReadyStateChange_, false, this);862}863864this.handleError_(goog.net.ErrorCode.ACCESS_DENIED);865}866867} else {868// For all other browsers we do some trickery to ensure that there is no869// entry on the history stack. Thanks go to jlim for the prototype for this870871goog.log.fine(this.logger_, 'Setting up iframes and cloning form');872873this.appendIframe_();874875var innerFrameName =876this.iframeName_ + goog.net.IframeIo.INNER_FRAME_SUFFIX;877878// Open and document.write another iframe into the iframe879var doc = goog.dom.getFrameContentDocument(this.iframe_);880var html;881if (document.baseURI) {882// On Safari 4 and 5 the new iframe doesn't inherit the current baseURI.883html = goog.net.IframeIo.createIframeHtmlWithBaseUri_(innerFrameName);884} else {885html = goog.net.IframeIo.createIframeHtml_(innerFrameName);886}887if (goog.userAgent.OPERA && !goog.userAgent.WEBKIT) {888// Presto based Opera adds a history entry when document.write is used.889// Change the innerHTML of the page instead.890goog.dom.safe.setInnerHtml(doc.documentElement, html);891} else {892goog.dom.safe.documentWrite(doc, html);893}894895// Listen for the iframe's load896if (!this.ignoreResponse_) {897goog.events.listen(898doc.getElementById(innerFrameName), goog.events.EventType.LOAD,899this.onIframeLoaded_, false, this);900}901902// Fix text areas, since importNode won't clone changes to the value903var textareas = goog.dom.getElementsByTagName(904goog.dom.TagName.TEXTAREA, goog.asserts.assert(this.form_));905for (var i = 0, n = textareas.length; i < n; i++) {906// The childnodes represent the initial child nodes for the text area907// appending a text node essentially resets the initial value ready for908// it to be clones - while maintaining HTML escaping.909var value = textareas[i].value;910if (goog.dom.getRawTextContent(textareas[i]) != value) {911goog.dom.setTextContent(textareas[i], value);912textareas[i].value = value;913}914}915916// Append a cloned form to the iframe917var clone = doc.importNode(this.form_, true);918clone.target = innerFrameName;919// Work around crbug.com/66987920clone.action = this.form_.action;921doc.body.appendChild(clone);922923// Fix select boxes, importNode won't override the default value924var selects = goog.dom.getElementsByTagName(925goog.dom.TagName.SELECT, goog.asserts.assert(this.form_));926var clones = goog.dom.getElementsByTagName(927goog.dom.TagName.SELECT, /** @type {!Element} */ (clone));928for (var i = 0, n = selects.length; i < n; i++) {929var selectsOptions =930goog.dom.getElementsByTagName(goog.dom.TagName.OPTION, selects[i]);931var clonesOptions =932goog.dom.getElementsByTagName(goog.dom.TagName.OPTION, clones[i]);933for (var j = 0, m = selectsOptions.length; j < m; j++) {934clonesOptions[j].selected = selectsOptions[j].selected;935}936}937938// IE and some versions of Firefox (1.5 - 1.5.07?) fail to clone the value939// attribute for <input type="file"> nodes, which results in an empty940// upload if the clone is submitted. Check, and if the clone failed, submit941// using the original form instead.942var inputs = goog.dom.getElementsByTagName(943goog.dom.TagName.INPUT, goog.asserts.assert(this.form_));944var inputClones = goog.dom.getElementsByTagName(945goog.dom.TagName.INPUT, /** @type {!Element} */ (clone));946for (var i = 0, n = inputs.length; i < n; i++) {947if (inputs[i].type == goog.dom.InputType.FILE) {948if (inputs[i].value != inputClones[i].value) {949goog.log.fine(950this.logger_, 'File input value not cloned properly. Will ' +951'submit using original form.');952this.form_.target = innerFrameName;953clone = this.form_;954break;955}956}957}958959goog.log.fine(this.logger_, 'Submitting form');960961962try {963this.errorHandled_ = false;964clone.submit();965doc.close();966967if (goog.userAgent.GECKO) {968// This tests if firefox silently fails, this can happen, for example,969// when the server resets the connection because of a large file upload970this.firefoxSilentErrorTimeout_ =971goog.Timer.callOnce(this.testForFirefoxSilentError_, 250, this);972}973974} catch (e) {975// If submit threw an exception then it probably means the page that the976// code is running on the local file system and the form's action was977// pointing to a file that doesn't exist, causing the browser to fire an978// exception.979980goog.log.error(981this.logger_,982'Error when submitting form: ' +983goog.debug.HtmlFormatter.exposeException(e));984985if (!this.ignoreResponse_) {986goog.events.unlisten(987doc.getElementById(innerFrameName), goog.events.EventType.LOAD,988this.onIframeLoaded_, false, this);989}990991doc.close();992993this.handleError_(goog.net.ErrorCode.FILE_NOT_FOUND);994}995}996};997998999/**1000* @param {string} innerFrameName1001* @return {!goog.html.SafeHtml}1002* @private1003*/1004goog.net.IframeIo.createIframeHtml_ = function(innerFrameName) {1005var innerFrameNameEscaped = goog.string.htmlEscape(innerFrameName);1006return goog.html.uncheckedconversions1007.safeHtmlFromStringKnownToSatisfyTypeContract(1008goog.string.Const.from(1009'Short HTML snippet, input escaped, for performance'),1010'<body><iframe id="' + innerFrameNameEscaped + '" name="' +1011innerFrameNameEscaped + '"></iframe>');1012};101310141015/**1016* @param {string} innerFrameName1017* @return {!goog.html.SafeHtml}1018* @private1019*/1020goog.net.IframeIo.createIframeHtmlWithBaseUri_ = function(innerFrameName) {1021var innerFrameNameEscaped = goog.string.htmlEscape(innerFrameName);1022return goog.html.uncheckedconversions1023.safeHtmlFromStringKnownToSatisfyTypeContract(1024goog.string.Const.from(1025'Short HTML snippet, input escaped, safe URL, for performance'),1026'<head><base href="' +1027goog.string.htmlEscape(/** @type {string} */ (document.baseURI)) +1028'"></head>' +1029'<body><iframe id="' + innerFrameNameEscaped + '" name="' +1030innerFrameNameEscaped + '"></iframe>');1031};103210331034/**1035* Handles the load event of the iframe for IE, determines if the request was1036* successful or not, handles clean up and dispatching of appropriate events.1037* @param {goog.events.BrowserEvent} e The browser event.1038* @private1039*/1040goog.net.IframeIo.prototype.onIeReadyStateChange_ = function(e) {1041if (this.iframe_.readyState == 'complete') {1042goog.events.unlisten(1043this.iframe_, goog.events.EventType.READYSTATECHANGE,1044this.onIeReadyStateChange_, false, this);1045var doc;10461047try {1048doc = goog.dom.getFrameContentDocument(this.iframe_);10491050// IE serves about:blank when it cannot load the resource while offline.1051if (goog.userAgent.IE && doc.location == 'about:blank' &&1052!navigator.onLine) {1053this.handleError_(goog.net.ErrorCode.OFFLINE);1054return;1055}1056} catch (ex) {1057this.handleError_(goog.net.ErrorCode.ACCESS_DENIED);1058return;1059}1060this.handleLoad_(/** @type {!HTMLDocument} */ (doc));1061}1062};106310641065/**1066* Handles the load event of the iframe for non-IE browsers.1067* @param {goog.events.BrowserEvent} e The browser event.1068* @private1069*/1070goog.net.IframeIo.prototype.onIframeLoaded_ = function(e) {1071// In Presto based Opera, the default "about:blank" page of iframes fires an1072// onload event that we'd like to ignore.1073if (goog.userAgent.OPERA && !goog.userAgent.WEBKIT &&1074this.getContentDocument_().location == 'about:blank') {1075return;1076}1077goog.events.unlisten(1078this.getRequestIframe(), goog.events.EventType.LOAD, this.onIframeLoaded_,1079false, this);1080try {1081this.handleLoad_(this.getContentDocument_());1082} catch (ex) {1083this.handleError_(goog.net.ErrorCode.ACCESS_DENIED);1084}1085};108610871088/**1089* Handles generic post-load1090* @param {HTMLDocument} contentDocument The frame's document.1091* @private1092*/1093goog.net.IframeIo.prototype.handleLoad_ = function(contentDocument) {1094goog.log.fine(this.logger_, 'Iframe loaded');10951096this.complete_ = true;1097this.active_ = false;10981099var errorCode;11001101// Try to get the innerHTML. If this fails then it can be an access denied1102// error or the document may just not have a body, typical case is if there1103// is an IE's default 404.11041105try {1106var body = contentDocument.body;1107this.lastContent_ = body.textContent || body.innerText;1108this.lastContentHtml_ = body.innerHTML;1109} catch (ex) {1110errorCode = goog.net.ErrorCode.ACCESS_DENIED;1111}11121113// Use a callback function, defined by the application, to analyse the1114// contentDocument and determine if it is an error page. Applications1115// may send down markers in the document, define JS vars, or some other test.1116var customError;1117if (!errorCode && typeof this.errorChecker_ == 'function') {1118customError = this.errorChecker_(contentDocument);1119if (customError) {1120errorCode = goog.net.ErrorCode.CUSTOM_ERROR;1121}1122}11231124goog.log.log(1125this.logger_, goog.log.Level.FINER, 'Last content: ' + this.lastContent_);1126goog.log.log(1127this.logger_, goog.log.Level.FINER, 'Last uri: ' + this.lastUri_);11281129if (errorCode) {1130goog.log.fine(this.logger_, 'Load event occurred but failed');1131this.handleError_(errorCode, customError);11321133} else {1134goog.log.fine(this.logger_, 'Load succeeded');1135this.success_ = true;1136this.lastErrorCode_ = goog.net.ErrorCode.NO_ERROR;1137this.dispatchEvent(goog.net.EventType.COMPLETE);1138this.dispatchEvent(goog.net.EventType.SUCCESS);11391140this.makeReady_();1141}1142};114311441145/**1146* Handles errors.1147* @param {goog.net.ErrorCode} errorCode Error code.1148* @param {Object=} opt_customError If error is CUSTOM_ERROR, this is the1149* client-provided custom error.1150* @private1151*/1152goog.net.IframeIo.prototype.handleError_ = function(1153errorCode, opt_customError) {1154if (!this.errorHandled_) {1155this.success_ = false;1156this.active_ = false;1157this.complete_ = true;1158this.lastErrorCode_ = errorCode;1159if (errorCode == goog.net.ErrorCode.CUSTOM_ERROR) {1160goog.asserts.assert(goog.isDef(opt_customError));1161this.lastCustomError_ = opt_customError;1162}1163this.dispatchEvent(goog.net.EventType.COMPLETE);1164this.dispatchEvent(goog.net.EventType.ERROR);11651166this.makeReady_();11671168this.errorHandled_ = true;1169}1170};117111721173/**1174* Dispatches an event indicating that the IframeIo instance has received a data1175* packet via incremental loading. The event object has a 'data' member.1176* @param {Object} data Data.1177* @private1178*/1179goog.net.IframeIo.prototype.handleIncrementalData_ = function(data) {1180this.dispatchEvent(new goog.net.IframeIo.IncrementalDataEvent(data));1181};118211831184/**1185* Finalizes the request, schedules the iframe for disposal, and maybe disposes1186* the form.1187* @private1188*/1189goog.net.IframeIo.prototype.makeReady_ = function() {1190goog.log.info(this.logger_, 'Ready for new requests');1191this.scheduleIframeDisposal_();1192this.disposeForm_();1193this.dispatchEvent(goog.net.EventType.READY);1194};119511961197/**1198* Creates an iframe to be used with a request. We use a new iframe for each1199* request so that requests don't create history entries.1200* @private1201*/1202goog.net.IframeIo.prototype.createIframe_ = function() {1203goog.log.fine(this.logger_, 'Creating iframe');12041205this.iframeName_ = this.name_ + '_' + (this.nextIframeId_++).toString(36);12061207var iframeAttributes = {'name': this.iframeName_, 'id': this.iframeName_};1208// Setting the source to javascript:"" is a fix to remove IE6 mixed content1209// warnings when being used in an https page.1210if (goog.userAgent.IE && Number(goog.userAgent.VERSION) < 7) {1211iframeAttributes.src = 'javascript:""';1212}12131214this.iframe_ = goog.dom.getDomHelper(this.form_).createDom(1215goog.dom.TagName.IFRAME, iframeAttributes);12161217var s = this.iframe_.style;1218s.visibility = 'hidden';1219s.width = s.height = '10px';1220// Chrome sometimes shows scrollbars when visibility is hidden, but not when1221// display is none.1222s.display = 'none';12231224// There are reports that safari 2.0.3 has a bug where absolutely positioned1225// iframes can't have their src set.1226if (!goog.userAgent.WEBKIT) {1227s.position = 'absolute';1228s.top = s.left = '-10px';1229} else {1230s.marginTop = s.marginLeft = '-10px';1231}1232};123312341235/**1236* Appends the Iframe to the document body.1237* @private1238*/1239goog.net.IframeIo.prototype.appendIframe_ = function() {1240goog.dom.getDomHelper(this.form_)1241.getDocument()1242.body.appendChild(this.iframe_);1243};124412451246/**1247* Schedules an iframe for disposal, async. We can't remove the iframes in the1248* same execution context as the response, otherwise some versions of Firefox1249* will not detect that the response has correctly finished and the loading bar1250* will stay active forever.1251* @private1252*/1253goog.net.IframeIo.prototype.scheduleIframeDisposal_ = function() {1254var iframe = this.iframe_;12551256// There shouldn't be a case where the iframe is null and we get to this1257// stage, but the error reports in http://b/909448 indicate it is possible.1258if (iframe) {1259// NOTE(user): Stops Internet Explorer leaking the iframe object. This1260// shouldn't be needed, since the events have all been removed, which1261// should in theory clean up references. Oh well...1262iframe.onreadystatechange = null;1263iframe.onload = null;1264iframe.onerror = null;12651266this.iframesForDisposal_.push(iframe);1267}12681269if (this.iframeDisposalTimer_) {1270goog.Timer.clear(this.iframeDisposalTimer_);1271this.iframeDisposalTimer_ = null;1272}12731274if (goog.userAgent.GECKO ||1275(goog.userAgent.OPERA && !goog.userAgent.WEBKIT)) {1276// For FF and Presto Opera, we must dispose the iframe async,1277// but it doesn't need to be done as soon as possible.1278// We therefore schedule it for 2s out, so as not to1279// affect any other actions that may have been triggered by the request.1280this.iframeDisposalTimer_ = goog.Timer.callOnce(1281this.disposeIframes_, goog.net.IframeIo.IFRAME_DISPOSE_DELAY_MS, this);12821283} else {1284// For non-Gecko browsers we dispose straight away.1285this.disposeIframes_();1286}12871288// Nullify reference1289this.iframe_ = null;1290this.iframeName_ = null;1291};129212931294/**1295* Disposes any iframes.1296* @private1297*/1298goog.net.IframeIo.prototype.disposeIframes_ = function() {1299if (this.iframeDisposalTimer_) {1300// Clear the timer1301goog.Timer.clear(this.iframeDisposalTimer_);1302this.iframeDisposalTimer_ = null;1303}13041305while (this.iframesForDisposal_.length != 0) {1306var iframe = this.iframesForDisposal_.pop();1307goog.log.info(this.logger_, 'Disposing iframe');1308goog.dom.removeNode(iframe);1309}1310};131113121313/**1314* Removes all the child nodes from the static form so it can be reused again.1315* This should happen right after sending a request. Otherwise, there can be1316* issues when another iframe uses this form right after the first iframe.1317* @private1318*/1319goog.net.IframeIo.prototype.clearForm_ = function() {1320if (this.form_ && this.form_ == goog.net.IframeIo.form_) {1321goog.dom.removeChildren(this.form_);1322}1323};132413251326/**1327* Disposes of the Form. Since IE6 leaks form nodes, this just cleans up the1328* DOM and nullifies the instances reference so the form can be used for another1329* request.1330* @private1331*/1332goog.net.IframeIo.prototype.disposeForm_ = function() {1333this.clearForm_();1334this.form_ = null;1335};133613371338/**1339* @return {HTMLDocument} The appropriate content document.1340* @private1341*/1342goog.net.IframeIo.prototype.getContentDocument_ = function() {1343if (this.iframe_) {1344return /** @type {!HTMLDocument} */ (1345goog.dom.getFrameContentDocument(this.getRequestIframe()));1346}1347return null;1348};134913501351/**1352* @return {HTMLIFrameElement} The appropriate iframe to use for requests1353* (created in sendForm_).1354*/1355goog.net.IframeIo.prototype.getRequestIframe = function() {1356if (this.iframe_) {1357return /** @type {HTMLIFrameElement} */ (1358goog.net.IframeIo.useIeReadyStateCodePath_() ?1359this.iframe_ :1360goog.dom.getFrameContentDocument(this.iframe_)1361.getElementById(1362this.iframeName_ + goog.net.IframeIo.INNER_FRAME_SUFFIX));1363}1364return null;1365};136613671368/**1369* Tests for a silent failure by firefox that can occur when the connection is1370* reset by the server or is made to an illegal URL.1371* @private1372*/1373goog.net.IframeIo.prototype.testForFirefoxSilentError_ = function() {1374if (this.active_) {1375var doc = this.getContentDocument_();13761377// This is a hack to test of the document has loaded with a page that1378// we can't access, such as a network error, that won't report onload1379// or onerror events.1380if (doc && !goog.reflect.canAccessProperty(doc, 'documentUri')) {1381if (!this.ignoreResponse_) {1382goog.events.unlisten(1383this.getRequestIframe(), goog.events.EventType.LOAD,1384this.onIframeLoaded_, false, this);1385}13861387if (navigator.onLine) {1388goog.log.warning(this.logger_, 'Silent Firefox error detected');1389this.handleError_(goog.net.ErrorCode.FF_SILENT_ERROR);1390} else {1391goog.log.warning(1392this.logger_, 'Firefox is offline so report offline error ' +1393'instead of silent error');1394this.handleError_(goog.net.ErrorCode.OFFLINE);1395}1396return;1397}1398this.firefoxSilentErrorTimeout_ =1399goog.Timer.callOnce(this.testForFirefoxSilentError_, 250, this);1400}1401};1402140314041405/**1406* Class for representing incremental data events.1407* @param {Object} data The data associated with the event.1408* @extends {goog.events.Event}1409* @constructor1410* @final1411*/1412goog.net.IframeIo.IncrementalDataEvent = function(data) {1413goog.events.Event.call(this, goog.net.EventType.INCREMENTAL_DATA);14141415/**1416* The data associated with the event.1417* @type {Object}1418*/1419this.data = data;1420};1421goog.inherits(goog.net.IframeIo.IncrementalDataEvent, goog.events.Event);142214231424