2 * Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14 * its contributors may be used to endorse or promote products derived
15 * from this software without specific prior written permission.
17 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 // This table maps MIME types to the Resource.Types which are valid for them.
30 // The following line:
31 // "text/html": {0: 1},
32 // means that text/html is a valid MIME type for resources that have type
33 // WebInspector.Resource.Type.Document (which has a value of 0).
34 WebInspector.MIMETypes = {
35 "text/html": {0: true},
36 "text/xml": {0: true},
37 "text/plain": {0: true},
38 "application/xhtml+xml": {0: true},
39 "text/css": {1: true},
40 "text/xsl": {1: true},
41 "image/jpeg": {2: true},
42 "image/png": {2: true},
43 "image/gif": {2: true},
44 "image/bmp": {2: true},
45 "image/svg+xml": {2: true},
46 "image/vnd.microsoft.icon": {2: true},
47 "image/webp": {2: true},
48 "image/x-icon": {2: true},
49 "image/x-xbitmap": {2: true},
50 "font/ttf": {3: true},
51 "font/opentype": {3: true},
52 "application/x-font-type1": {3: true},
53 "application/x-font-ttf": {3: true},
54 "application/x-font-woff": {3: true},
55 "application/x-truetype-font": {3: true},
56 "text/javascript": {4: true},
57 "text/ecmascript": {4: true},
58 "application/javascript": {4: true},
59 "application/ecmascript": {4: true},
60 "application/x-javascript": {4: true},
61 "application/json": {4: true},
62 "text/javascript1.1": {4: true},
63 "text/javascript1.2": {4: true},
64 "text/javascript1.3": {4: true},
65 "text/jscript": {4: true},
66 "text/livescript": {4: true},
71 * @extends {WebInspector.Object}
73 * @param {NetworkAgent.RequestId} requestId
75 * @param {?string} frameId
76 * @param {?NetworkAgent.LoaderId} loaderId
78 WebInspector.Resource = function(requestId, url, frameId, loaderId)
80 this.requestId = requestId;
82 this.frameId = frameId;
83 this.loaderId = loaderId;
86 this._category = WebInspector.resourceCategories.other;
87 this._pendingContentCallbacks = [];
92 this.requestMethod = "";
94 this.receiveHeadersEnd = 0;
97 WebInspector.Resource.displayName = function(url)
99 return new WebInspector.Resource("fake-transient-resource", url, null, null).displayName;
102 // Keep these in sync with WebCore::InspectorResource::Type
103 WebInspector.Resource.Type = {
113 isTextType: function(type)
115 return (type === WebInspector.Resource.Type.Document) || (type === WebInspector.Resource.Type.Stylesheet) || (type === WebInspector.Resource.Type.Script) || (type === WebInspector.Resource.Type.XHR);
118 toUIString: function(type)
121 case WebInspector.Resource.Type.Document:
122 return WebInspector.UIString("Document");
123 case WebInspector.Resource.Type.Stylesheet:
124 return WebInspector.UIString("Stylesheet");
125 case WebInspector.Resource.Type.Image:
126 return WebInspector.UIString("Image");
127 case WebInspector.Resource.Type.Font:
128 return WebInspector.UIString("Font");
129 case WebInspector.Resource.Type.Script:
130 return WebInspector.UIString("Script");
131 case WebInspector.Resource.Type.XHR:
132 return WebInspector.UIString("XHR");
133 case WebInspector.Resource.Type.WebSocket:
134 return WebInspector.UIString("WebSocket");
135 case WebInspector.Resource.Type.Other:
137 return WebInspector.UIString("Other");
141 // Returns locale-independent string identifier of resource type (primarily for use in extension API).
142 // The IDs need to be kept in sync with webInspector.resoureces.Types object in ExtensionAPI.js.
143 toString: function(type)
146 case WebInspector.Resource.Type.Document:
148 case WebInspector.Resource.Type.Stylesheet:
150 case WebInspector.Resource.Type.Image:
152 case WebInspector.Resource.Type.Font:
154 case WebInspector.Resource.Type.Script:
156 case WebInspector.Resource.Type.XHR:
158 case WebInspector.Resource.Type.WebSocket:
160 case WebInspector.Resource.Type.Other:
167 WebInspector.Resource._domainModelBindings = [];
169 WebInspector.Resource.registerDomainModelBinding = function(type, binding)
171 WebInspector.Resource._domainModelBindings[type] = binding;
174 WebInspector.Resource._resourceRevisionRegistry = function()
176 if (!WebInspector.Resource._resourceRevisionRegistryObject) {
177 if (window.localStorage) {
178 var resourceHistory = window.localStorage["resource-history"];
180 WebInspector.Resource._resourceRevisionRegistryObject = resourceHistory ? JSON.parse(resourceHistory) : {};
182 WebInspector.Resource._resourceRevisionRegistryObject = {};
185 WebInspector.Resource._resourceRevisionRegistryObject = {};
187 return WebInspector.Resource._resourceRevisionRegistryObject;
190 WebInspector.Resource.restoreRevisions = function()
192 var registry = WebInspector.Resource._resourceRevisionRegistry();
193 var filteredRegistry = {};
194 for (var url in registry) {
195 var historyItems = registry[url];
196 var resource = WebInspector.resourceForURL(url);
198 var filteredHistoryItems = [];
199 for (var i = 0; historyItems && i < historyItems.length; ++i) {
200 var historyItem = historyItems[i];
201 if (resource && historyItem.loaderId === resource.loaderId) {
202 resource.addRevision(window.localStorage[historyItem.key], new Date(historyItem.timestamp), true);
203 filteredHistoryItems.push(historyItem);
204 filteredRegistry[url] = filteredHistoryItems;
206 delete window.localStorage[historyItem.key];
209 WebInspector.Resource._resourceRevisionRegistryObject = filteredRegistry;
213 window.localStorage["resource-history"] = JSON.stringify(filteredRegistry);
216 // Schedule async storage.
217 setTimeout(persist, 0);}
219 WebInspector.Resource.persistRevision = function(resource)
221 if (!window.localStorage)
224 var url = resource.url;
225 var loaderId = resource.loaderId;
226 var timestamp = resource._contentTimestamp.getTime();
227 var key = "resource-history|" + url + "|" + loaderId + "|" + timestamp;
228 var content = resource._content;
230 var registry = WebInspector.Resource._resourceRevisionRegistry();
232 var historyItems = registry[resource.url];
235 registry[resource.url] = historyItems;
237 historyItems.push({url: url, loaderId: loaderId, timestamp: timestamp, key: key});
241 window.localStorage[key] = content;
242 window.localStorage["resource-history"] = JSON.stringify(registry);
245 // Schedule async storage.
246 setTimeout(persist, 0);
249 WebInspector.Resource.Events = {
250 RevisionAdded: "revision-added",
251 MessageAdded: "message-added",
252 MessagesCleared: "messages-cleared",
255 WebInspector.Resource.prototype = {
267 delete this._parsedQueryParameters;
269 var parsedURL = x.asParsedURL();
270 this.domain = parsedURL ? parsedURL.host : "";
271 this.path = parsedURL ? parsedURL.path : "";
272 this.urlFragment = parsedURL ? parsedURL.fragment : "";
273 this.lastPathComponent = parsedURL ? parsedURL.lastPathComponent : "";
274 this.lastPathComponentLowerCase = this.lastPathComponent.toLowerCase();
279 return this._documentURL;
284 this._documentURL = x;
289 if (this._displayName)
290 return this._displayName;
291 this._displayName = this.lastPathComponent;
292 if (!this._displayName)
293 this._displayName = this.displayDomain;
294 if (!this._displayName && this.url)
295 this._displayName = this.url.trimURL(WebInspector.inspectedPageDomain ? WebInspector.inspectedPageDomain : "");
296 if (this._displayName === "/")
297 this._displayName = this.url;
298 return this._displayName;
303 var path = this.path;
304 var indexOfQuery = path.indexOf("?");
305 if (indexOfQuery !== -1)
306 path = path.substring(0, indexOfQuery);
307 var lastSlashIndex = path.lastIndexOf("/");
308 return lastSlashIndex !== -1 ? path.substring(0, lastSlashIndex) : "";
313 // WebInspector.Database calls this, so don't access more than this.domain.
314 if (this.domain && (!WebInspector.inspectedPageDomain || (WebInspector.inspectedPageDomain && this.domain !== WebInspector.inspectedPageDomain)))
321 return this._startTime || -1;
329 get responseReceivedTime()
331 return this._responseReceivedTime || -1;
334 set responseReceivedTime(x)
336 this._responseReceivedTime = x;
341 return this._endTime || -1;
346 if (this.timing && this.timing.requestTime) {
347 // Check against accurate responseReceivedTime.
348 this._endTime = Math.max(x, this.responseReceivedTime);
350 // Prefer endTime since it might be from the network stack.
352 if (this._responseReceivedTime > x)
353 this._responseReceivedTime = x;
359 if (this._endTime === -1 || this._startTime === -1)
361 return this._endTime - this._startTime;
366 if (this._responseReceivedTime === -1 || this._startTime === -1)
368 return this._responseReceivedTime - this._startTime;
371 get receiveDuration()
373 if (this._endTime === -1 || this._responseReceivedTime === -1)
375 return this._endTime - this._responseReceivedTime;
380 return this._resourceSize || 0;
385 this._resourceSize = x;
392 if (this.statusCode === 304) // Not modified
393 return this.responseHeadersSize;
394 if (this._transferSize !== undefined)
395 return this._transferSize;
396 // If we did not receive actual transfer size from network
397 // stack, we prefer using Content-Length over resourceSize as
398 // resourceSize may differ from actual transfer size if platform's
399 // network stack performed decoding (e.g. gzip decompression).
400 // The Content-Length, though, is expected to come from raw
401 // response headers and will reflect actual transfer length.
402 // This won't work for chunked content encoding, so fall back to
403 // resourceSize when we don't have Content-Length. This still won't
404 // work for chunks with non-trivial encodings. We need a way to
405 // get actual transfer size from the network stack.
406 var bodySize = Number(this.responseHeaders["Content-Length"] || this.resourceSize);
407 return this.responseHeadersSize + bodySize;
410 increaseTransferSize: function(x)
412 this._transferSize = (this._transferSize || 0) + x;
417 return this._finished;
422 if (this._finished === x)
428 this.dispatchEventToListeners("finished");
429 if (this._pendingContentCallbacks.length)
430 this._innerRequestContent();
446 return this._canceled;
456 return this._category;
483 if (x && !this._cached) {
484 // Take startTime and responseReceivedTime from timing data for better accuracy.
485 // Timing's requestTime is a baseline in seconds, rest of the numbers there are ticks in millis.
486 this._startTime = x.requestTime;
487 this._responseReceivedTime = x.requestTime + x.receiveHeadersEnd / 1000.0;
490 this.dispatchEventToListeners("timing changed");
496 return this._mimeType;
511 if (this._type === x)
517 case WebInspector.Resource.Type.Document:
518 this.category = WebInspector.resourceCategories.documents;
520 case WebInspector.Resource.Type.Stylesheet:
521 this.category = WebInspector.resourceCategories.stylesheets;
523 case WebInspector.Resource.Type.Script:
524 this.category = WebInspector.resourceCategories.scripts;
526 case WebInspector.Resource.Type.Image:
527 this.category = WebInspector.resourceCategories.images;
529 case WebInspector.Resource.Type.Font:
530 this.category = WebInspector.resourceCategories.fonts;
532 case WebInspector.Resource.Type.XHR:
533 this.category = WebInspector.resourceCategories.xhr;
535 case WebInspector.Resource.Type.WebSocket:
536 this.category = WebInspector.resourceCategories.websockets;
538 case WebInspector.Resource.Type.Other:
540 this.category = WebInspector.resourceCategories.other;
547 if (this.redirects && this.redirects.length > 0)
548 return this.redirects[this.redirects.length - 1];
549 return this._redirectSource;
552 set redirectSource(x)
554 this._redirectSource = x;
559 return this._requestHeaders || {};
562 set requestHeaders(x)
564 this._requestHeaders = x;
565 delete this._sortedRequestHeaders;
566 delete this._requestCookies;
568 this.dispatchEventToListeners("requestHeaders changed");
571 get requestHeadersText()
573 if (this._requestHeadersText === undefined) {
574 this._requestHeadersText = this.requestMethod + " " + this.url + " HTTP/1.1\r\n";
575 for (var key in this.requestHeaders)
576 this._requestHeadersText += key + ": " + this.requestHeaders[key] + "\r\n";
578 return this._requestHeadersText;
581 set requestHeadersText(x)
583 this._requestHeadersText = x;
585 this.dispatchEventToListeners("requestHeaders changed");
588 get requestHeadersSize()
590 return this.requestHeadersText.length;
593 get sortedRequestHeaders()
595 if (this._sortedRequestHeaders !== undefined)
596 return this._sortedRequestHeaders;
598 this._sortedRequestHeaders = [];
599 for (var key in this.requestHeaders)
600 this._sortedRequestHeaders.push({header: key, value: this.requestHeaders[key]});
601 this._sortedRequestHeaders.sort(function(a,b) { return a.header.localeCompare(b.header) });
603 return this._sortedRequestHeaders;
606 requestHeaderValue: function(headerName)
608 return this._headerValue(this.requestHeaders, headerName);
613 if (!this._requestCookies)
614 this._requestCookies = WebInspector.CookieParser.parseCookie(this.requestHeaderValue("Cookie"));
615 return this._requestCookies;
618 get requestFormData()
620 return this._requestFormData;
623 set requestFormData(x)
625 this._requestFormData = x;
626 delete this._parsedFormParameters;
629 get requestHttpVersion()
631 var firstLine = this.requestHeadersText.split(/\r\n/)[0];
632 var match = firstLine.match(/(HTTP\/\d+\.\d+)$/);
633 return match ? match[1] : undefined;
636 get responseHeaders()
638 return this._responseHeaders || {};
641 set responseHeaders(x)
643 this._responseHeaders = x;
644 delete this._sortedResponseHeaders;
645 delete this._responseCookies;
647 this.dispatchEventToListeners("responseHeaders changed");
650 get responseHeadersText()
652 if (this._responseHeadersText === undefined) {
653 this._responseHeadersText = "HTTP/1.1 " + this.statusCode + " " + this.statusText + "\r\n";
654 for (var key in this.responseHeaders)
655 this._responseHeadersText += key + ": " + this.responseHeaders[key] + "\r\n";
657 return this._responseHeadersText;
660 set responseHeadersText(x)
662 this._responseHeadersText = x;
664 this.dispatchEventToListeners("responseHeaders changed");
667 get responseHeadersSize()
669 return this.responseHeadersText.length;
672 get sortedResponseHeaders()
674 if (this._sortedResponseHeaders !== undefined)
675 return this._sortedResponseHeaders;
677 this._sortedResponseHeaders = [];
678 for (var key in this.responseHeaders)
679 this._sortedResponseHeaders.push({header: key, value: this.responseHeaders[key]});
680 this._sortedResponseHeaders.sort(function(a,b) { return a.header.localeCompare(b.header) });
682 return this._sortedResponseHeaders;
685 responseHeaderValue: function(headerName)
687 return this._headerValue(this.responseHeaders, headerName);
690 get responseCookies()
692 if (!this._responseCookies)
693 this._responseCookies = WebInspector.CookieParser.parseSetCookie(this.responseHeaderValue("Set-Cookie"));
694 return this._responseCookies;
697 get queryParameters()
699 if (this._parsedQueryParameters)
700 return this._parsedQueryParameters;
701 var queryString = this.url.split("?", 2)[1];
704 queryString = queryString.split("#", 2)[0];
705 this._parsedQueryParameters = this._parseParameters(queryString);
706 return this._parsedQueryParameters;
711 if (this._parsedFormParameters)
712 return this._parsedFormParameters;
713 if (!this.requestFormData)
715 var requestContentType = this.requestContentType();
716 if (!requestContentType || !requestContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i))
718 this._parsedFormParameters = this._parseParameters(this.requestFormData);
719 return this._parsedFormParameters;
722 get responseHttpVersion()
724 var match = this.responseHeadersText.match(/^(HTTP\/\d+\.\d+)/);
725 return match ? match[1] : undefined;
728 _parseParameters: function(queryString)
730 function parseNameValue(pair)
733 var splitPair = pair.split("=", 2);
735 parameter.name = splitPair[0];
736 if (splitPair.length === 1)
737 parameter.value = "";
739 parameter.value = splitPair[1];
742 return queryString.split("&").map(parseNameValue);
745 _headerValue: function(headers, headerName)
747 headerName = headerName.toLowerCase();
748 for (var header in headers) {
749 if (header.toLowerCase() === headerName)
750 return headers[header];
756 return this._messages || [];
759 addMessage: function(msg)
761 if (!msg.isErrorOrWarning() || !msg.message)
766 this._messages.push(msg);
767 this.dispatchEventToListeners(WebInspector.Resource.Events.MessageAdded, msg);
772 return this._errors || 0;
782 return this._warnings || 0;
790 clearErrorsAndWarnings: function()
795 this.dispatchEventToListeners(WebInspector.Resource.Events.MessagesCleared);
800 return this._content;
805 return this._contentEncoded;
808 get contentTimestamp()
810 return this._contentTimestamp;
813 isEditable: function()
815 if (this._actualResource)
817 var binding = WebInspector.Resource._domainModelBindings[this.type];
818 return binding && binding.canSetContent(this);
821 setContent: function(newContent, majorChange, callback)
823 if (!this.isEditable()) {
825 callback("Resource is not editable");
828 var binding = WebInspector.Resource._domainModelBindings[this.type];
829 binding.setContent(this, newContent, majorChange, callback);
833 * @param {string} newContent
834 * @param {Date=} timestamp
835 * @param {boolean=} restoringHistory
837 addRevision: function(newContent, timestamp, restoringHistory)
839 var revision = new WebInspector.ResourceRevision(this, this._content, this._contentTimestamp);
840 this.history.push(revision);
842 this._content = newContent;
843 this._contentTimestamp = timestamp || new Date();
845 this.dispatchEventToListeners(WebInspector.Resource.Events.RevisionAdded, revision);
846 if (!restoringHistory)
847 this._persistRevision();
848 WebInspector.resourceTreeModel.dispatchEventToListeners(WebInspector.ResourceTreeModel.EventTypes.ResourceContentCommitted, { resource: this, content: newContent });
851 _persistRevision: function()
853 WebInspector.Resource.persistRevision(this);
856 requestContent: function(callback)
858 // We do not support content retrieval for WebSockets at the moment.
859 // Since WebSockets are potentially long-living, fail requests immediately
860 // to prevent caller blocking until resource is marked as finished.
861 if (this.type === WebInspector.Resource.Type.WebSocket) {
862 callback(null, null);
865 if (typeof this._content !== "undefined") {
866 callback(this.content, this._contentEncoded);
869 this._pendingContentCallbacks.push(callback);
871 this._innerRequestContent();
874 searchInContent: function(query, caseSensitive, isRegex, callback)
876 function callbackWrapper(error, searchMatches)
878 callback(searchMatches || []);
882 PageAgent.searchInResource(this.frameId, this.url, query, caseSensitive, isRegex, callbackWrapper);
887 populateImageSource: function(image)
889 function onResourceContent()
891 image.src = this._contentURL();
894 this.requestContent(onResourceContent.bind(this));
897 isHttpFamily: function()
899 return this.url.match(/^https?:/i);
902 requestContentType: function()
904 return this.requestHeaderValue("Content-Type");
907 isPingRequest: function()
909 return "text/ping" === this.requestContentType();
912 hasErrorStatusCode: function()
914 return this.statusCode >= 400;
917 _contentURL: function()
919 const maxDataUrlSize = 1024 * 1024;
920 // If resource content is not available or won't fit a data URL, fall back to using original URL.
921 if (this._content == null || this._content.length > maxDataUrlSize)
924 return "data:" + this.mimeType + (this._contentEncoded ? ";base64," : ",") + this._content;
927 _innerRequestContent: function()
929 if (this._contentRequested)
931 this._contentRequested = true;
933 function onResourceContent(data, contentEncoded)
935 this._contentEncoded = contentEncoded;
936 this._content = data;
937 this._originalContent = data;
938 var callbacks = this._pendingContentCallbacks.slice();
939 for (var i = 0; i < callbacks.length; ++i)
940 callbacks[i](this._content, this._contentEncoded);
941 this._pendingContentCallbacks.length = 0;
942 delete this._contentRequested;
944 WebInspector.networkManager.requestContent(this, onResourceContent.bind(this));
948 WebInspector.Resource.prototype.__proto__ = WebInspector.Object.prototype;
953 WebInspector.ResourceRevision = function(resource, content, timestamp)
955 this._resource = resource;
956 this._content = content;
957 this._timestamp = timestamp;
960 WebInspector.ResourceRevision.prototype = {
963 return this._resource;
968 return this._timestamp;
973 return this._content;
976 revertToThis: function()
978 function revert(content)
980 this._resource.setContent(content, true);
982 this.requestContent(revert.bind(this));
985 requestContent: function(callback)
987 if (typeof this._content === "string") {
988 callback(this._content);
992 // If we are here, this is initial revision. First, look up content fetched over the wire.
993 if (typeof this.resource._originalContent === "string") {
994 this._content = this._resource._originalContent;
995 callback(this._content);
999 // If unsuccessful, request the content.
1000 function mycallback(content)
1002 this._content = content;
1005 WebInspector.networkManager.requestContent(this._resource, mycallback.bind(this));
1012 WebInspector.ResourceDomainModelBinding = function() { }
1013 WebInspector.ResourceDomainModelBinding.prototype = {
1014 canSetContent: function() { return true; },
1015 setContent: function(resource, content, majorChange, callback) { }