blob: d8373cd26eb299eaa12cbdf572c0cf22e0f7c749 [file] [log] [blame]
/*
* 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.history = [];
}
// 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._domainModelBindings = [];
WebInspector.Resource.registerDomainModelBinding = function(type, binding)
{
WebInspector.Resource._domainModelBindings[type] = binding;
}
WebInspector.Resource.Events = {
RevisionAdded: 0
}
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;
if (this._transferSize !== undefined)
return this._transferSize;
// If we did not receive actual transfer size from network
// stack, 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 actual transfer size from the network stack.
var bodySize = Number(this.responseHeaders["Content-Length"] || this.resourceSize);
return this.responseHeadersSize + bodySize;
},
increaseTransferSize: function(x)
{
this._transferSize = (this._transferSize || 0) + 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 canceled()
{
return this._canceled;
},
set canceled(x)
{
this._canceled = 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;
delete this._responseHeadersSize;
this.dispatchEventToListeners("requestHeaders changed");
},
get requestHeadersText()
{
return this._requestHeadersText;
},
set requestHeadersText(x)
{
this._requestHeadersText = x;
delete this._responseHeadersSize;
this.dispatchEventToListeners("requestHeaders changed");
},
get requestHeadersSize()
{
if (typeof(this._requestHeadersSize) === "undefined") {
if (this._requestHeadersText)
this._requestHeadersSize = this._requestHeadersText.length;
else
this._requestHeadersSize = this._headersSize(this._requestHeaders)
}
return this._requestHeadersSize;
},
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;
delete this._responseHeadersSize;
delete this._sortedResponseHeaders;
delete this._responseCookies;
this.dispatchEventToListeners("responseHeaders changed");
},
get responseHeadersText()
{
return this._responseHeadersText;
},
set responseHeadersText(x)
{
this._responseHeadersText = x;
delete this._responseHeadersSize;
this.dispatchEventToListeners("responseHeaders changed");
},
get responseHeadersSize()
{
if (typeof(this._responseHeadersSize) === "undefined") {
if (this._responseHeadersText)
this._responseHeadersSize = this._responseHeadersText.length;
else
this._responseHeadersSize = this._headersSize(this._responseHeaders)
}
return this._responseHeadersSize;
},
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)
{
// We should take actual headers size from network stack, when possible, but fall back to
// this lousy computation when no headers text is available.
var size = 0;
for (var header in headers)
size += header.length + headers[header].length + 4; // _typical_ overhead per header is ": ".length + "\r\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.
// Also, if a URL like http://localhost/wiki/load.php?debug=true&lang=en produces text/css and gets reloaded,
// it is 304 Not Modified and its guessed mime-type is text/php, which is wrong.
// Don't check for mime-types in 304-resources.
if (this.statusCode >= 400 || this.statusCode === 304)
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;
},
isEditable: function()
{
if (this._actualResource)
return false;
var binding = WebInspector.Resource._domainModelBindings[this.type];
return binding && binding.canSetContent(this);
},
setContent: function(newContent, majorChange, callback)
{
if (!this.isEditable(this)) {
if (callback)
callback("Resource is not editable");
return;
}
var binding = WebInspector.Resource._domainModelBindings[this.type];
binding.setContent(this, newContent, majorChange, callback);
},
addRevision: function(newContent)
{
var revision = new WebInspector.ResourceRevision(this, this._content, this._contentTimestamp);
this.history.push(revision);
this._content = newContent;
this._contentTimestamp = new Date();
this.dispatchEventToListeners(WebInspector.Resource.Events.RevisionAdded, revision);
},
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;
},
isDataURL: function()
{
return this.url.match(/^data:/i);
},
_contentURL: function()
{
const 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;
this._originalContent = 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;
WebInspector.ResourceRevision = function(resource, content, timestamp)
{
this._resource = resource;
this._content = content;
this._timestamp = timestamp;
}
WebInspector.ResourceRevision.prototype = {
get resource()
{
return this._resource;
},
get timestamp()
{
return this._timestamp;
},
get content()
{
return this._content;
},
revertToThis: function()
{
function revert(content)
{
this._resource.setContent(content, true);
}
this.requestContent(revert.bind(this));
},
requestContent: function(callback)
{
if (typeof this._content === "string") {
callback(this._content);
return;
}
// If we are here, this is initial revision. First, look up content fetched over the wire.
if (typeof this.resource._originalContent === "string") {
this._content = this._resource._originalContent;
callback(this._content);
return;
}
// If unsuccessful, request the content.
function mycallback(content)
{
this._content = content;
callback(content);
}
WebInspector.networkManager.requestContent(this._resource, false, mycallback.bind(this));
}
}
WebInspector.ResourceDomainModelBinding = function()
{
}
WebInspector.ResourceDomainModelBinding.prototype = {
canSetContent: function()
{
// Implemented by the domains.
return true;
},
setContent: function(resource, content, majorChange, callback)
{
// Implemented by the domains.
}
}