1/*
2 * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 *
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.
16 *
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.
27 */
28WebInspector.Resource = function(identifier, url)
29{
30    this.identifier = identifier;
31    this.url = url;
32    this._startTime = -1;
33    this._endTime = -1;
34    this._category = WebInspector.resourceCategories.other;
35    this._pendingContentCallbacks = [];
36    this.history = [];
37}
38
39// Keep these in sync with WebCore::InspectorResource::Type
40WebInspector.Resource.Type = {
41    Document:   0,
42    Stylesheet: 1,
43    Image:      2,
44    Font:       3,
45    Script:     4,
46    XHR:        5,
47    WebSocket:  7,
48    Other:      8,
49
50    isTextType: function(type)
51    {
52        return (type === this.Document) || (type === this.Stylesheet) || (type === this.Script) || (type === this.XHR);
53    },
54
55    toUIString: function(type)
56    {
57        switch (type) {
58            case this.Document:
59                return WebInspector.UIString("Document");
60            case this.Stylesheet:
61                return WebInspector.UIString("Stylesheet");
62            case this.Image:
63                return WebInspector.UIString("Image");
64            case this.Font:
65                return WebInspector.UIString("Font");
66            case this.Script:
67                return WebInspector.UIString("Script");
68            case this.XHR:
69                return WebInspector.UIString("XHR");
70            case this.WebSocket:
71                return WebInspector.UIString("WebSocket");
72            case this.Other:
73            default:
74                return WebInspector.UIString("Other");
75        }
76    },
77
78    // Returns locale-independent string identifier of resource type (primarily for use in extension API).
79    // The IDs need to be kept in sync with webInspector.resoureces.Types object in ExtensionAPI.js.
80    toString: function(type)
81    {
82        switch (type) {
83            case this.Document:
84                return "document";
85            case this.Stylesheet:
86                return "stylesheet";
87            case this.Image:
88                return "image";
89            case this.Font:
90                return "font";
91            case this.Script:
92                return "script";
93            case this.XHR:
94                return "xhr";
95            case this.WebSocket:
96                return "websocket";
97            case this.Other:
98            default:
99                return "other";
100        }
101    }
102}
103
104WebInspector.Resource._domainModelBindings = [];
105
106WebInspector.Resource.registerDomainModelBinding = function(type, binding)
107{
108    WebInspector.Resource._domainModelBindings[type] = binding;
109}
110
111WebInspector.Resource.Events = {
112    RevisionAdded: 0
113}
114
115WebInspector.Resource.prototype = {
116    get url()
117    {
118        return this._url;
119    },
120
121    set url(x)
122    {
123        if (this._url === x)
124            return;
125
126        this._url = x;
127        delete this._parsedQueryParameters;
128
129        var parsedURL = x.asParsedURL();
130        this.domain = parsedURL ? parsedURL.host : "";
131        this.path = parsedURL ? parsedURL.path : "";
132        this.lastPathComponent = "";
133        if (parsedURL && parsedURL.path) {
134            // First cut the query params.
135            var path = parsedURL.path;
136            var indexOfQuery = path.indexOf("?");
137            if (indexOfQuery !== -1)
138                path = path.substring(0, indexOfQuery);
139
140            // Then take last path component.
141            var lastSlashIndex = path.lastIndexOf("/");
142            if (lastSlashIndex !== -1)
143                this.lastPathComponent = path.substring(lastSlashIndex + 1);
144        }
145        this.lastPathComponentLowerCase = this.lastPathComponent.toLowerCase();
146    },
147
148    get documentURL()
149    {
150        return this._documentURL;
151    },
152
153    set documentURL(x)
154    {
155        this._documentURL = x;
156    },
157
158    get displayName()
159    {
160        if (this._displayName)
161            return this._displayName;
162        this._displayName = this.lastPathComponent;
163        if (!this._displayName)
164            this._displayName = this.displayDomain;
165        if (!this._displayName && this.url)
166            this._displayName = this.url.trimURL(WebInspector.mainResource ? WebInspector.mainResource.domain : "");
167        if (this._displayName === "/")
168            this._displayName = this.url;
169        return this._displayName;
170    },
171
172    get displayDomain()
173    {
174        // WebInspector.Database calls this, so don't access more than this.domain.
175        if (this.domain && (!WebInspector.mainResource || (WebInspector.mainResource && this.domain !== WebInspector.mainResource.domain)))
176            return this.domain;
177        return "";
178    },
179
180    get startTime()
181    {
182        return this._startTime || -1;
183    },
184
185    set startTime(x)
186    {
187        this._startTime = x;
188    },
189
190    get responseReceivedTime()
191    {
192        return this._responseReceivedTime || -1;
193    },
194
195    set responseReceivedTime(x)
196    {
197        this._responseReceivedTime = x;
198    },
199
200    get endTime()
201    {
202        return this._endTime || -1;
203    },
204
205    set endTime(x)
206    {
207        if (this.timing && this.timing.requestTime) {
208            // Check against accurate responseReceivedTime.
209            this._endTime = Math.max(x, this.responseReceivedTime);
210        } else {
211            // Prefer endTime since it might be from the network stack.
212            this._endTime = x;
213            if (this._responseReceivedTime > x)
214                this._responseReceivedTime = x;
215        }
216    },
217
218    get duration()
219    {
220        if (this._endTime === -1 || this._startTime === -1)
221            return -1;
222        return this._endTime - this._startTime;
223    },
224
225    get latency()
226    {
227        if (this._responseReceivedTime === -1 || this._startTime === -1)
228            return -1;
229        return this._responseReceivedTime - this._startTime;
230    },
231
232    get receiveDuration()
233    {
234        if (this._endTime === -1 || this._responseReceivedTime === -1)
235            return -1;
236        return this._endTime - this._responseReceivedTime;
237    },
238
239    get resourceSize()
240    {
241        return this._resourceSize || 0;
242    },
243
244    set resourceSize(x)
245    {
246        this._resourceSize = x;
247    },
248
249    get transferSize()
250    {
251        if (this.cached)
252            return 0;
253        if (this.statusCode === 304) // Not modified
254            return this.responseHeadersSize;
255        if (this._transferSize !== undefined)
256            return this._transferSize;
257        // If we did not receive actual transfer size from network
258        // stack, we prefer using Content-Length over resourceSize as
259        // resourceSize may differ from actual transfer size if platform's
260        // network stack performed decoding (e.g. gzip decompression).
261        // The Content-Length, though, is expected to come from raw
262        // response headers and will reflect actual transfer length.
263        // This won't work for chunked content encoding, so fall back to
264        // resourceSize when we don't have Content-Length. This still won't
265        // work for chunks with non-trivial encodings. We need a way to
266        // get actual transfer size from the network stack.
267        var bodySize = Number(this.responseHeaders["Content-Length"] || this.resourceSize);
268        return this.responseHeadersSize + bodySize;
269    },
270
271    increaseTransferSize: function(x)
272    {
273        this._transferSize = (this._transferSize || 0) + x;
274    },
275
276    get finished()
277    {
278        return this._finished;
279    },
280
281    set finished(x)
282    {
283        if (this._finished === x)
284            return;
285
286        this._finished = x;
287
288        if (x) {
289            this._checkWarnings();
290            this.dispatchEventToListeners("finished");
291            if (this._pendingContentCallbacks.length)
292                this._innerRequestContent();
293        }
294    },
295
296    get failed()
297    {
298        return this._failed;
299    },
300
301    set failed(x)
302    {
303        this._failed = x;
304    },
305
306    get canceled()
307    {
308        return this._canceled;
309    },
310
311    set canceled(x)
312    {
313        this._canceled = x;
314    },
315
316    get category()
317    {
318        return this._category;
319    },
320
321    set category(x)
322    {
323        this._category = x;
324    },
325
326    get cached()
327    {
328        return this._cached;
329    },
330
331    set cached(x)
332    {
333        this._cached = x;
334        if (x)
335            delete this._timing;
336    },
337
338    get timing()
339    {
340        return this._timing;
341    },
342
343    set timing(x)
344    {
345        if (x && !this._cached) {
346            // Take startTime and responseReceivedTime from timing data for better accuracy.
347            // Timing's requestTime is a baseline in seconds, rest of the numbers there are ticks in millis.
348            this._startTime = x.requestTime;
349            this._responseReceivedTime = x.requestTime + x.receiveHeadersEnd / 1000.0;
350
351            this._timing = x;
352            this.dispatchEventToListeners("timing changed");
353        }
354    },
355
356    get mimeType()
357    {
358        return this._mimeType;
359    },
360
361    set mimeType(x)
362    {
363        this._mimeType = x;
364    },
365
366    get type()
367    {
368        return this._type;
369    },
370
371    set type(x)
372    {
373        if (this._type === x)
374            return;
375
376        this._type = x;
377
378        switch (x) {
379            case WebInspector.Resource.Type.Document:
380                this.category = WebInspector.resourceCategories.documents;
381                break;
382            case WebInspector.Resource.Type.Stylesheet:
383                this.category = WebInspector.resourceCategories.stylesheets;
384                break;
385            case WebInspector.Resource.Type.Script:
386                this.category = WebInspector.resourceCategories.scripts;
387                break;
388            case WebInspector.Resource.Type.Image:
389                this.category = WebInspector.resourceCategories.images;
390                break;
391            case WebInspector.Resource.Type.Font:
392                this.category = WebInspector.resourceCategories.fonts;
393                break;
394            case WebInspector.Resource.Type.XHR:
395                this.category = WebInspector.resourceCategories.xhr;
396                break;
397            case WebInspector.Resource.Type.WebSocket:
398                this.category = WebInspector.resourceCategories.websockets;
399                break;
400            case WebInspector.Resource.Type.Other:
401            default:
402                this.category = WebInspector.resourceCategories.other;
403                break;
404        }
405    },
406
407    get requestHeaders()
408    {
409        return this._requestHeaders || {};
410    },
411
412    set requestHeaders(x)
413    {
414        this._requestHeaders = x;
415        delete this._sortedRequestHeaders;
416        delete this._requestCookies;
417        delete this._responseHeadersSize;
418
419        this.dispatchEventToListeners("requestHeaders changed");
420    },
421
422    get requestHeadersText()
423    {
424        return this._requestHeadersText;
425    },
426
427    set requestHeadersText(x)
428    {
429        this._requestHeadersText = x;
430        delete this._responseHeadersSize;
431
432        this.dispatchEventToListeners("requestHeaders changed");
433    },
434
435    get requestHeadersSize()
436    {
437        if (typeof(this._requestHeadersSize) === "undefined") {
438            if (this._requestHeadersText)
439                this._requestHeadersSize = this._requestHeadersText.length;
440            else
441                this._requestHeadersSize = this._headersSize(this._requestHeaders)
442        }
443        return this._requestHeadersSize;
444    },
445
446    get sortedRequestHeaders()
447    {
448        if (this._sortedRequestHeaders !== undefined)
449            return this._sortedRequestHeaders;
450
451        this._sortedRequestHeaders = [];
452        for (var key in this.requestHeaders)
453            this._sortedRequestHeaders.push({header: key, value: this.requestHeaders[key]});
454        this._sortedRequestHeaders.sort(function(a,b) { return a.header.localeCompare(b.header) });
455
456        return this._sortedRequestHeaders;
457    },
458
459    requestHeaderValue: function(headerName)
460    {
461        return this._headerValue(this.requestHeaders, headerName);
462    },
463
464    get requestCookies()
465    {
466        if (!this._requestCookies)
467            this._requestCookies = WebInspector.CookieParser.parseCookie(this.requestHeaderValue("Cookie"));
468        return this._requestCookies;
469    },
470
471    get requestFormData()
472    {
473        return this._requestFormData;
474    },
475
476    set requestFormData(x)
477    {
478        this._requestFormData = x;
479        delete this._parsedFormParameters;
480    },
481
482    get responseHeaders()
483    {
484        return this._responseHeaders || {};
485    },
486
487    set responseHeaders(x)
488    {
489        this._responseHeaders = x;
490        delete this._responseHeadersSize;
491        delete this._sortedResponseHeaders;
492        delete this._responseCookies;
493
494        this.dispatchEventToListeners("responseHeaders changed");
495    },
496
497    get responseHeadersText()
498    {
499        return this._responseHeadersText;
500    },
501
502    set responseHeadersText(x)
503    {
504        this._responseHeadersText = x;
505        delete this._responseHeadersSize;
506
507        this.dispatchEventToListeners("responseHeaders changed");
508    },
509
510    get responseHeadersSize()
511    {
512        if (typeof(this._responseHeadersSize) === "undefined") {
513            if (this._responseHeadersText)
514                this._responseHeadersSize = this._responseHeadersText.length;
515            else
516                this._responseHeadersSize = this._headersSize(this._responseHeaders)
517        }
518        return this._responseHeadersSize;
519    },
520
521
522    get sortedResponseHeaders()
523    {
524        if (this._sortedResponseHeaders !== undefined)
525            return this._sortedResponseHeaders;
526
527        this._sortedResponseHeaders = [];
528        for (var key in this.responseHeaders)
529            this._sortedResponseHeaders.push({header: key, value: this.responseHeaders[key]});
530        this._sortedResponseHeaders.sort(function(a,b) { return a.header.localeCompare(b.header) });
531
532        return this._sortedResponseHeaders;
533    },
534
535    responseHeaderValue: function(headerName)
536    {
537        return this._headerValue(this.responseHeaders, headerName);
538    },
539
540    get responseCookies()
541    {
542        if (!this._responseCookies)
543            this._responseCookies = WebInspector.CookieParser.parseSetCookie(this.responseHeaderValue("Set-Cookie"));
544        return this._responseCookies;
545    },
546
547    get queryParameters()
548    {
549        if (this._parsedQueryParameters)
550            return this._parsedQueryParameters;
551        var queryString = this.url.split("?", 2)[1];
552        if (!queryString)
553            return;
554        this._parsedQueryParameters = this._parseParameters(queryString);
555        return this._parsedQueryParameters;
556    },
557
558    get formParameters()
559    {
560        if (this._parsedFormParameters)
561            return this._parsedFormParameters;
562        if (!this.requestFormData)
563            return;
564        var requestContentType = this.requestHeaderValue("Content-Type");
565        if (!requestContentType || !requestContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i))
566            return;
567        this._parsedFormParameters = this._parseParameters(this.requestFormData);
568        return this._parsedFormParameters;
569    },
570
571    _parseParameters: function(queryString)
572    {
573        function parseNameValue(pair)
574        {
575            var parameter = {};
576            var splitPair = pair.split("=", 2);
577
578            parameter.name = splitPair[0];
579            if (splitPair.length === 1)
580                parameter.value = "";
581            else
582                parameter.value = splitPair[1];
583            return parameter;
584        }
585        return queryString.split("&").map(parseNameValue);
586    },
587
588    _headerValue: function(headers, headerName)
589    {
590        headerName = headerName.toLowerCase();
591        for (var header in headers) {
592            if (header.toLowerCase() === headerName)
593                return headers[header];
594        }
595    },
596
597    _headersSize: function(headers)
598    {
599        // We should take actual headers size from network stack, when possible, but fall back to
600        // this lousy computation when no headers text is available.
601        var size = 0;
602        for (var header in headers)
603            size += header.length + headers[header].length + 4; // _typical_ overhead per header is ": ".length + "\r\n".length.
604        return size;
605    },
606
607    get errors()
608    {
609        return this._errors || 0;
610    },
611
612    set errors(x)
613    {
614        this._errors = x;
615        this.dispatchEventToListeners("errors-warnings-updated");
616    },
617
618    get warnings()
619    {
620        return this._warnings || 0;
621    },
622
623    set warnings(x)
624    {
625        this._warnings = x;
626        this.dispatchEventToListeners("errors-warnings-updated");
627    },
628
629    clearErrorsAndWarnings: function()
630    {
631        this._warnings = 0;
632        this._errors = 0;
633        this.dispatchEventToListeners("errors-warnings-updated");
634    },
635
636    _mimeTypeIsConsistentWithType: function()
637    {
638        // If status is an error, content is likely to be of an inconsistent type,
639        // as it's going to be an error message. We do not want to emit a warning
640        // for this, though, as this will already be reported as resource loading failure.
641        // Also, if a URL like http://localhost/wiki/load.php?debug=true&lang=en produces text/css and gets reloaded,
642        // it is 304 Not Modified and its guessed mime-type is text/php, which is wrong.
643        // Don't check for mime-types in 304-resources.
644        if (this.statusCode >= 400 || this.statusCode === 304)
645            return true;
646
647        if (typeof this.type === "undefined"
648            || this.type === WebInspector.Resource.Type.Other
649            || this.type === WebInspector.Resource.Type.XHR
650            || this.type === WebInspector.Resource.Type.WebSocket)
651            return true;
652
653        if (!this.mimeType)
654            return true; // Might be not known for cached resources with null responses.
655
656        if (this.mimeType in WebInspector.MIMETypes)
657            return this.type in WebInspector.MIMETypes[this.mimeType];
658
659        return false;
660    },
661
662    _checkWarnings: function()
663    {
664        for (var warning in WebInspector.Warnings)
665            this._checkWarning(WebInspector.Warnings[warning]);
666    },
667
668    _checkWarning: function(warning)
669    {
670        var msg;
671        switch (warning.id) {
672            case WebInspector.Warnings.IncorrectMIMEType.id:
673                if (!this._mimeTypeIsConsistentWithType())
674                    msg = new WebInspector.ConsoleMessage(WebInspector.ConsoleMessage.MessageSource.Other,
675                        WebInspector.ConsoleMessage.MessageType.Log,
676                        WebInspector.ConsoleMessage.MessageLevel.Warning,
677                        -1,
678                        this.url,
679                        1,
680                        String.sprintf(WebInspector.Warnings.IncorrectMIMEType.message, WebInspector.Resource.Type.toUIString(this.type), this.mimeType),
681                        null,
682                        null);
683                break;
684        }
685
686        if (msg)
687            WebInspector.console.addMessage(msg);
688    },
689
690    get content()
691    {
692        return this._content;
693    },
694
695    get contentTimestamp()
696    {
697        return this._contentTimestamp;
698    },
699
700    setInitialContent: function(content)
701    {
702        this._content = content;
703    },
704
705    isEditable: function()
706    {
707        if (this._actualResource)
708            return false;
709        var binding = WebInspector.Resource._domainModelBindings[this.type];
710        return binding && binding.canSetContent(this);
711    },
712
713    setContent: function(newContent, majorChange, callback)
714    {
715        if (!this.isEditable(this)) {
716            if (callback)
717                callback("Resource is not editable");
718            return;
719        }
720        var binding = WebInspector.Resource._domainModelBindings[this.type];
721        binding.setContent(this, newContent, majorChange, callback);
722    },
723
724    addRevision: function(newContent)
725    {
726        var revision = new WebInspector.ResourceRevision(this, this._content, this._contentTimestamp);
727        this.history.push(revision);
728
729        this._content = newContent;
730        this._contentTimestamp = new Date();
731
732        this.dispatchEventToListeners(WebInspector.Resource.Events.RevisionAdded, revision);
733    },
734
735    requestContent: function(callback)
736    {
737        // We do not support content retrieval for WebSockets at the moment.
738        // Since WebSockets are potentially long-living, fail requests immediately
739        // to prevent caller blocking until resource is marked as finished.
740        if (this.type === WebInspector.Resource.Type.WebSocket) {
741            callback(null, null);
742            return;
743        }
744        if (typeof this._content !== "undefined") {
745            callback(this.content, this._contentEncoded);
746            return;
747        }
748        this._pendingContentCallbacks.push(callback);
749        if (this.finished)
750            this._innerRequestContent();
751    },
752
753    populateImageSource: function(image)
754    {
755        function onResourceContent()
756        {
757            image.src = this._contentURL();
758        }
759
760        if (Preferences.useDataURLForResourceImageIcons)
761            this.requestContent(onResourceContent.bind(this));
762        else
763            image.src = this.url;
764    },
765
766    isDataURL: function()
767    {
768        return this.url.match(/^data:/i);
769    },
770
771    _contentURL: function()
772    {
773        const maxDataUrlSize = 1024 * 1024;
774        // If resource content is not available or won't fit a data URL, fall back to using original URL.
775        if (this._content == null || this._content.length > maxDataUrlSize)
776            return this.url;
777
778        return "data:" + this.mimeType + (this._contentEncoded ? ";base64," : ",") + this._content;
779    },
780
781    _innerRequestContent: function()
782    {
783        if (this._contentRequested)
784            return;
785        this._contentRequested = true;
786        this._contentEncoded = !WebInspector.Resource.Type.isTextType(this.type);
787
788        function onResourceContent(data)
789        {
790            this._content = data;
791            this._originalContent = data;
792            var callbacks = this._pendingContentCallbacks.slice();
793            for (var i = 0; i < callbacks.length; ++i)
794                callbacks[i](this._content, this._contentEncoded);
795            this._pendingContentCallbacks.length = 0;
796            delete this._contentRequested;
797        }
798        WebInspector.networkManager.requestContent(this, this._contentEncoded, onResourceContent.bind(this));
799    }
800}
801
802WebInspector.Resource.prototype.__proto__ = WebInspector.Object.prototype;
803
804WebInspector.ResourceRevision = function(resource, content, timestamp)
805{
806    this._resource = resource;
807    this._content = content;
808    this._timestamp = timestamp;
809}
810
811WebInspector.ResourceRevision.prototype = {
812    get resource()
813    {
814        return this._resource;
815    },
816
817    get timestamp()
818    {
819        return this._timestamp;
820    },
821
822    get content()
823    {
824        return this._content;
825    },
826
827    revertToThis: function()
828    {
829        function revert(content)
830        {
831            this._resource.setContent(content, true);
832        }
833        this.requestContent(revert.bind(this));
834    },
835
836    requestContent: function(callback)
837    {
838        if (typeof this._content === "string") {
839            callback(this._content);
840            return;
841        }
842
843        // If we are here, this is initial revision. First, look up content fetched over the wire.
844        if (typeof this.resource._originalContent === "string") {
845            this._content = this._resource._originalContent;
846            callback(this._content);
847            return;
848        }
849
850        // If unsuccessful, request the content.
851        function mycallback(content)
852        {
853            this._content = content;
854            callback(content);
855        }
856        WebInspector.networkManager.requestContent(this._resource, false, mycallback.bind(this));
857    }
858}
859
860WebInspector.ResourceDomainModelBinding = function()
861{
862}
863
864WebInspector.ResourceDomainModelBinding.prototype = {
865    canSetContent: function()
866    {
867        // Implemented by the domains.
868        return true;
869    },
870
871    setContent: function(resource, content, majorChange, callback)
872    {
873        // Implemented by the domains.
874    }
875}
876