/* * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ WebInspector.Resource = function(identifier, url) { this.identifier = identifier; this.url = url; this._startTime = -1; this._endTime = -1; this._category = WebInspector.resourceCategories.other; this._pendingContentCallbacks = []; this._responseHeadersSize = 0; } // Keep these in sync with WebCore::InspectorResource::Type WebInspector.Resource.Type = { Document: 0, Stylesheet: 1, Image: 2, Font: 3, Script: 4, XHR: 5, WebSocket: 7, Other: 8, isTextType: function(type) { return (type === this.Document) || (type === this.Stylesheet) || (type === this.Script) || (type === this.XHR); }, toUIString: function(type) { switch (type) { case this.Document: return WebInspector.UIString("Document"); case this.Stylesheet: return WebInspector.UIString("Stylesheet"); case this.Image: return WebInspector.UIString("Image"); case this.Font: return WebInspector.UIString("Font"); case this.Script: return WebInspector.UIString("Script"); case this.XHR: return WebInspector.UIString("XHR"); case this.WebSocket: return WebInspector.UIString("WebSocket"); case this.Other: default: return WebInspector.UIString("Other"); } }, // Returns locale-independent string identifier of resource type (primarily for use in extension API). // The IDs need to be kept in sync with webInspector.resoureces.Types object in ExtensionAPI.js. toString: function(type) { switch (type) { case this.Document: return "document"; case this.Stylesheet: return "stylesheet"; case this.Image: return "image"; case this.Font: return "font"; case this.Script: return "script"; case this.XHR: return "xhr"; case this.WebSocket: return "websocket"; case this.Other: default: return "other"; } } } WebInspector.Resource.prototype = { get url() { return this._url; }, set url(x) { if (this._url === x) return; this._url = x; delete this._parsedQueryParameters; var parsedURL = x.asParsedURL(); this.domain = parsedURL ? parsedURL.host : ""; this.path = parsedURL ? parsedURL.path : ""; this.lastPathComponent = ""; if (parsedURL && parsedURL.path) { // First cut the query params. var path = parsedURL.path; var indexOfQuery = path.indexOf("?"); if (indexOfQuery !== -1) path = path.substring(0, indexOfQuery); // Then take last path component. var lastSlashIndex = path.lastIndexOf("/"); if (lastSlashIndex !== -1) this.lastPathComponent = path.substring(lastSlashIndex + 1); } this.lastPathComponentLowerCase = this.lastPathComponent.toLowerCase(); }, get documentURL() { return this._documentURL; }, set documentURL(x) { this._documentURL = x; }, get displayName() { if (this._displayName) return this._displayName; this._displayName = this.lastPathComponent; if (!this._displayName) this._displayName = this.displayDomain; if (!this._displayName && this.url) this._displayName = this.url.trimURL(WebInspector.mainResource ? WebInspector.mainResource.domain : ""); if (this._displayName === "/") this._displayName = this.url; return this._displayName; }, get displayDomain() { // WebInspector.Database calls this, so don't access more than this.domain. if (this.domain && (!WebInspector.mainResource || (WebInspector.mainResource && this.domain !== WebInspector.mainResource.domain))) return this.domain; return ""; }, get startTime() { return this._startTime || -1; }, set startTime(x) { this._startTime = x; }, get responseReceivedTime() { return this._responseReceivedTime || -1; }, set responseReceivedTime(x) { this._responseReceivedTime = x; }, get endTime() { return this._endTime || -1; }, set endTime(x) { if (this.timing && this.timing.requestTime) { // Check against accurate responseReceivedTime. this._endTime = Math.max(x, this.responseReceivedTime); } else { // Prefer endTime since it might be from the network stack. this._endTime = x; if (this._responseReceivedTime > x) this._responseReceivedTime = x; } }, get duration() { if (this._endTime === -1 || this._startTime === -1) return -1; return this._endTime - this._startTime; }, get latency() { if (this._responseReceivedTime === -1 || this._startTime === -1) return -1; return this._responseReceivedTime - this._startTime; }, get receiveDuration() { if (this._endTime === -1 || this._responseReceivedTime === -1) return -1; return this._endTime - this._responseReceivedTime; }, get resourceSize() { return this._resourceSize || 0; }, set resourceSize(x) { this._resourceSize = x; }, get transferSize() { if (this.cached) return 0; if (this.statusCode === 304) // Not modified return this._responseHeadersSize; // FIXME: We prefer using Content-Length over resourceSize as // resourceSize may differ from actual transfer size if platform's // network stack performed decoding (e.g. gzip decompression). // The Content-Length, though, is expected to come from raw // response headers and will reflect actual transfer length. // This won't work for chunked content encoding, so fall back to // resourceSize when we don't have Content-Length. This still won't // work for chunks with non-trivial encodings. We need a way to // get actaul transfer size from the network stack. var bodySize = Number(this.responseHeaders["Content-Length"] || this.resourceSize); return this._responseHeadersSize + bodySize; }, get expectedContentLength() { return this._expectedContentLength || 0; }, set expectedContentLength(x) { this._expectedContentLength = x; }, get finished() { return this._finished; }, set finished(x) { if (this._finished === x) return; this._finished = x; if (x) { this._checkWarnings(); this.dispatchEventToListeners("finished"); if (this._pendingContentCallbacks.length) this._innerRequestContent(); } }, get failed() { return this._failed; }, set failed(x) { this._failed = x; }, get category() { return this._category; }, set category(x) { this._category = x; }, get cached() { return this._cached; }, set cached(x) { this._cached = x; if (x) delete this._timing; }, get timing() { return this._timing; }, set timing(x) { if (x && !this._cached) { // Take startTime and responseReceivedTime from timing data for better accuracy. // Timing's requestTime is a baseline in seconds, rest of the numbers there are ticks in millis. this._startTime = x.requestTime; this._responseReceivedTime = x.requestTime + x.receiveHeadersEnd / 1000.0; this._timing = x; this.dispatchEventToListeners("timing changed"); } }, get mimeType() { return this._mimeType; }, set mimeType(x) { this._mimeType = x; }, get type() { return this._type; }, set type(x) { if (this._type === x) return; this._type = x; switch (x) { case WebInspector.Resource.Type.Document: this.category = WebInspector.resourceCategories.documents; break; case WebInspector.Resource.Type.Stylesheet: this.category = WebInspector.resourceCategories.stylesheets; break; case WebInspector.Resource.Type.Script: this.category = WebInspector.resourceCategories.scripts; break; case WebInspector.Resource.Type.Image: this.category = WebInspector.resourceCategories.images; break; case WebInspector.Resource.Type.Font: this.category = WebInspector.resourceCategories.fonts; break; case WebInspector.Resource.Type.XHR: this.category = WebInspector.resourceCategories.xhr; break; case WebInspector.Resource.Type.WebSocket: this.category = WebInspector.resourceCategories.websockets; break; case WebInspector.Resource.Type.Other: default: this.category = WebInspector.resourceCategories.other; break; } }, get requestHeaders() { return this._requestHeaders || {}; }, set requestHeaders(x) { this._requestHeaders = x; delete this._sortedRequestHeaders; delete this._requestCookies; this.dispatchEventToListeners("requestHeaders changed"); }, get sortedRequestHeaders() { if (this._sortedRequestHeaders !== undefined) return this._sortedRequestHeaders; this._sortedRequestHeaders = []; for (var key in this.requestHeaders) this._sortedRequestHeaders.push({header: key, value: this.requestHeaders[key]}); this._sortedRequestHeaders.sort(function(a,b) { return a.header.localeCompare(b.header) }); return this._sortedRequestHeaders; }, requestHeaderValue: function(headerName) { return this._headerValue(this.requestHeaders, headerName); }, get requestCookies() { if (!this._requestCookies) this._requestCookies = WebInspector.CookieParser.parseCookie(this.requestHeaderValue("Cookie")); return this._requestCookies; }, get requestFormData() { return this._requestFormData; }, set requestFormData(x) { this._requestFormData = x; delete this._parsedFormParameters; }, get responseHeaders() { return this._responseHeaders || {}; }, set responseHeaders(x) { this._responseHeaders = x; // FIXME: we should take actual headers size from network stack, when possible. this._responseHeadersSize = this._headersSize(x); delete this._sortedResponseHeaders; delete this._responseCookies; this.dispatchEventToListeners("responseHeaders changed"); }, get sortedResponseHeaders() { if (this._sortedResponseHeaders !== undefined) return this._sortedResponseHeaders; this._sortedResponseHeaders = []; for (var key in this.responseHeaders) this._sortedResponseHeaders.push({header: key, value: this.responseHeaders[key]}); this._sortedResponseHeaders.sort(function(a,b) { return a.header.localeCompare(b.header) }); return this._sortedResponseHeaders; }, responseHeaderValue: function(headerName) { return this._headerValue(this.responseHeaders, headerName); }, get responseCookies() { if (!this._responseCookies) this._responseCookies = WebInspector.CookieParser.parseSetCookie(this.responseHeaderValue("Set-Cookie")); return this._responseCookies; }, get queryParameters() { if (this._parsedQueryParameters) return this._parsedQueryParameters; var queryString = this.url.split("?", 2)[1]; if (!queryString) return; this._parsedQueryParameters = this._parseParameters(queryString); return this._parsedQueryParameters; }, get formParameters() { if (this._parsedFormParameters) return this._parsedFormParameters; if (!this.requestFormData) return; var requestContentType = this.requestHeaderValue("Content-Type"); if (!requestContentType || !requestContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i)) return; this._parsedFormParameters = this._parseParameters(this.requestFormData); return this._parsedFormParameters; }, _parseParameters: function(queryString) { function parseNameValue(pair) { var parameter = {}; var splitPair = pair.split("=", 2); parameter.name = splitPair[0]; if (splitPair.length === 1) parameter.value = ""; else parameter.value = splitPair[1]; return parameter; } return queryString.split("&").map(parseNameValue); }, _headerValue: function(headers, headerName) { headerName = headerName.toLowerCase(); for (var header in headers) { if (header.toLowerCase() === headerName) return headers[header]; } }, _headersSize: function(headers) { var size = 0; for (var header in headers) size += header.length + headers[header].length + 3; // _typical_ overhead per herader is ": ".length + "\n".length. return size; }, get errors() { return this._errors || 0; }, set errors(x) { this._errors = x; this.dispatchEventToListeners("errors-warnings-updated"); }, get warnings() { return this._warnings || 0; }, set warnings(x) { this._warnings = x; this.dispatchEventToListeners("errors-warnings-updated"); }, clearErrorsAndWarnings: function() { this._warnings = 0; this._errors = 0; this.dispatchEventToListeners("errors-warnings-updated"); }, _mimeTypeIsConsistentWithType: function() { // If status is an error, content is likely to be of an inconsistent type, // as it's going to be an error message. We do not want to emit a warning // for this, though, as this will already be reported as resource loading failure. if (this.statusCode >= 400) return true; if (typeof this.type === "undefined" || this.type === WebInspector.Resource.Type.Other || this.type === WebInspector.Resource.Type.XHR || this.type === WebInspector.Resource.Type.WebSocket) return true; if (!this.mimeType) return true; // Might be not known for cached resources with null responses. if (this.mimeType in WebInspector.MIMETypes) return this.type in WebInspector.MIMETypes[this.mimeType]; return false; }, _checkWarnings: function() { for (var warning in WebInspector.Warnings) this._checkWarning(WebInspector.Warnings[warning]); }, _checkWarning: function(warning) { var msg; switch (warning.id) { case WebInspector.Warnings.IncorrectMIMEType.id: if (!this._mimeTypeIsConsistentWithType()) msg = new WebInspector.ConsoleMessage(WebInspector.ConsoleMessage.MessageSource.Other, WebInspector.ConsoleMessage.MessageType.Log, WebInspector.ConsoleMessage.MessageLevel.Warning, -1, this.url, 1, String.sprintf(WebInspector.Warnings.IncorrectMIMEType.message, WebInspector.Resource.Type.toUIString(this.type), this.mimeType), null, null); break; } if (msg) WebInspector.console.addMessage(msg); }, get content() { return this._content; }, get contentTimestamp() { return this._contentTimestamp; }, setInitialContent: function(content) { this._content = content; }, isLocallyModified: function() { return !!this._baseRevision; }, setContent: function(newContent, onRevert) { var revisionResource = new WebInspector.Resource(null, this.url); revisionResource.type = this.type; revisionResource.loader = this.loader; revisionResource.timestamp = this.timestamp; revisionResource._content = this._content; revisionResource._actualResource = this; revisionResource._fireOnRevert = onRevert; if (this.finished) revisionResource.finished = true; else { function finished() { this.removeEventListener("finished", finished); revisionResource.finished = true; } this.addEventListener("finished", finished.bind(this)); } if (!this._baseRevision) this._baseRevision = revisionResource; else revisionResource._baseRevision = this._baseRevision; var data = { revision: revisionResource }; this._content = newContent; this.timestamp = new Date(); this.dispatchEventToListeners("content-changed", data); }, revertToThis: function() { if (!this._actualResource || !this._fireOnRevert) return; function callback(content) { if (content) this._fireOnRevert(content); } this.requestContent(callback.bind(this)); }, get baseRevision() { return this._baseRevision; }, requestContent: function(callback) { // We do not support content retrieval for WebSockets at the moment. // Since WebSockets are potentially long-living, fail requests immediately // to prevent caller blocking until resource is marked as finished. if (this.type === WebInspector.Resource.Type.WebSocket) { callback(null, null); return; } if (typeof this._content !== "undefined") { callback(this.content, this._contentEncoded); return; } this._pendingContentCallbacks.push(callback); if (this.finished) this._innerRequestContent(); }, populateImageSource: function(image) { function onResourceContent() { image.src = this._contentURL(); } if (Preferences.useDataURLForResourceImageIcons) this.requestContent(onResourceContent.bind(this)); else image.src = this.url; }, _contentURL: function() { var maxDataUrlSize = 1024 * 1024; // If resource content is not available or won't fit a data URL, fall back to using original URL. if (this._content == null || this._content.length > maxDataUrlSize) return this.url; return "data:" + this.mimeType + (this._contentEncoded ? ";base64," : ",") + this._content; }, _innerRequestContent: function() { if (this._contentRequested) return; this._contentRequested = true; this._contentEncoded = !WebInspector.Resource.Type.isTextType(this.type); function onResourceContent(data) { this._content = data; var callbacks = this._pendingContentCallbacks.slice(); for (var i = 0; i < callbacks.length; ++i) callbacks[i](this._content, this._contentEncoded); this._pendingContentCallbacks.length = 0; delete this._contentRequested; } WebInspector.networkManager.requestContent(this, this._contentEncoded, onResourceContent.bind(this)); } } WebInspector.Resource.prototype.__proto__ = WebInspector.Object.prototype;