Web Inspector: provide a way to view XML/HTML/SVG resource responses as a DOM tree
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / ResourceClusterContentView.js
1 /*
2  * Copyright (C) 2013, 2015 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  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.ResourceClusterContentView = class ResourceClusterContentView extends WI.ClusterContentView
27 {
28     constructor(resource)
29     {
30         super(resource);
31
32         this._resource = resource;
33         this._resource.addEventListener(WI.Resource.Event.TypeDidChange, this._resourceTypeDidChange, this);
34         this._resource.addEventListener(WI.Resource.Event.LoadingDidFinish, this._resourceLoadingDidFinish, this);
35
36         this._responsePathComponent = this._createPathComponent({
37             displayName: WI.UIString("Response"),
38             identifier: ResourceClusterContentView.Identifier.Response,
39             styleClassNames: ["response-icon"],
40         });
41
42         if (this._canShowRequestContentView()) {
43             this._requestPathComponent = this._createPathComponent({
44                 displayName: WI.UIString("Request"),
45                 identifier: ResourceClusterContentView.Identifier.Request,
46                 styleClassNames: ["request-icon"],
47                 nextSibling: this._responsePathComponent,
48             });
49
50             this._tryEnableCustomRequestContentViews();
51         }
52
53         // FIXME: Since a custom response content view may only become available after a response is received
54         // we need to figure out a way to restore / prefer the custom content view. For example if users
55         // always want to prefer the JSON view to the normal Response text view.
56
57         this._currentContentViewSetting = new WI.Setting("resource-current-view-" + this._resource.url.hash, ResourceClusterContentView.Identifier.Response);
58
59         this._tryEnableCustomResponseContentViews();
60     }
61
62     // Public
63
64     get resource() { return this._resource; }
65
66     get selectionPathComponents()
67     {
68         let currentContentView = this._contentViewContainer.currentContentView;
69         if (!currentContentView)
70             return [];
71
72         if (!this._canShowRequestContentView() && !this._canShowCustomRequestContentView() && !this._canShowCustomResponseContentView())
73             return currentContentView.selectionPathComponents;
74
75         // Append the current view's path components to the path component representing the current view.
76         let components = [this._pathComponentForContentView(currentContentView)];
77         return components.concat(currentContentView.selectionPathComponents);
78     }
79
80     shown()
81     {
82         super.shown();
83
84         if (this._shownInitialContent)
85             return;
86
87         this._showContentViewForIdentifier(this._currentContentViewSetting.value);
88     }
89
90     closed()
91     {
92         super.closed();
93
94         this._shownInitialContent = false;
95     }
96
97     restoreFromCookie(cookie)
98     {
99         let contentView = this._showContentViewForIdentifier(cookie[WI.ResourceClusterContentView.ContentViewIdentifierCookieKey]);
100         if (typeof contentView.revealPosition === "function" && "lineNumber" in cookie && "columnNumber" in cookie)
101             contentView.revealPosition(new WI.SourceCodePosition(cookie.lineNumber, cookie.columnNumber));
102     }
103
104     showRequest()
105     {
106         this._shownInitialContent = true;
107
108         return this._showContentViewForIdentifier(ResourceClusterContentView.Identifier.Request);
109     }
110
111     showResponse(positionToReveal, textRangeToSelect, forceUnformatted)
112     {
113         this._shownInitialContent = true;
114
115         if (!this._resource.finished) {
116             this._positionToReveal = positionToReveal;
117             this._textRangeToSelect = textRangeToSelect;
118             this._forceUnformatted = forceUnformatted;
119         }
120
121         let responseContentView = this._showContentViewForIdentifier(ResourceClusterContentView.Identifier.Response);
122         if (typeof responseContentView.revealPosition === "function")
123             responseContentView.revealPosition(positionToReveal, textRangeToSelect, forceUnformatted);
124         return responseContentView;
125     }
126
127     // Private
128
129     get requestContentView()
130     {
131         if (!this._canShowRequestContentView())
132             return null;
133
134         if (this._requestContentView)
135             return this._requestContentView;
136
137         this._requestContentView = new WI.TextContentView(this._resource.requestData || "", this._resource.requestDataContentType);
138
139         return this._requestContentView;
140     }
141
142     get responseContentView()
143     {
144         if (this._responseContentView)
145             return this._responseContentView;
146
147         this._responseContentView = this._contentViewForResourceType(this._resource.type);
148         if (this._responseContentView)
149             return this._responseContentView;
150
151         let typeFromMIMEType = WI.Resource.typeFromMIMEType(this._resource.mimeType);
152         this._responseContentView = this._contentViewForResourceType(typeFromMIMEType);
153         if (this._responseContentView)
154             return this._responseContentView;
155
156         if (WI.shouldTreatMIMETypeAsText(this._resource.mimeType)) {
157             this._responseContentView = new WI.TextResourceContentView(this._resource);
158             return this._responseContentView;
159         }
160
161         this._responseContentView = new WI.GenericResourceContentView(this._resource);
162         return this._responseContentView;
163     }
164
165     get customRequestDOMContentView()
166     {
167         if (!this._customRequestDOMContentView && this._customRequestDOMContentViewInitializer)
168             this._customRequestDOMContentView = this._customRequestDOMContentViewInitializer();
169         return this._customRequestDOMContentView;
170     }
171
172     get customRequestJSONContentView()
173     {
174         if (!this._customRequestJSONContentView && this._customRequestJSONContentViewInitializer)
175             this._customRequestJSONContentView = this._customRequestJSONContentViewInitializer();
176         return this._customRequestJSONContentView;
177     }
178
179     get customResponseDOMContentView()
180     {
181         if (!this._customResponseDOMContentView && this._customResponseDOMContentViewInitializer)
182             this._customResponseDOMContentView = this._customResponseDOMContentViewInitializer();
183         return this._customResponseDOMContentView;
184     }
185
186     get customResponseJSONContentView()
187     {
188         if (!this._customResponseJSONContentView && this._customResponseJSONContentViewInitializer)
189             this._customResponseJSONContentView = this._customResponseJSONContentViewInitializer();
190         return this._customResponseJSONContentView;
191     }
192
193     get customResponseTextContentView()
194     {
195         if (!this._customResponseTextContentView && this._customResponseTextContentViewInitializer)
196             this._customResponseTextContentView = this._customResponseTextContentViewInitializer();
197         return this._customResponseTextContentView;
198     }
199
200     _createPathComponent({displayName, styleClassNames, identifier, previousSibling, nextSibling})
201     {
202         const textOnly = false;
203         const showSelectorArrows = true;
204         let pathComponent = new WI.HierarchicalPathComponent(displayName, styleClassNames, identifier, textOnly, showSelectorArrows);
205         pathComponent.comparisonData = this._resource;
206
207         if (previousSibling) {
208             previousSibling.nextSibling = pathComponent;
209             pathComponent.previousSibling = previousSibling;
210         }
211
212         if (nextSibling) {
213             nextSibling.previousSibling = pathComponent;
214             pathComponent.nextSibling = nextSibling;
215         }
216
217         pathComponent.addEventListener(WI.HierarchicalPathComponent.Event.SiblingWasSelected, this._pathComponentSelected, this);
218
219         return pathComponent;
220     }
221
222     _canShowRequestContentView()
223     {
224         let requestData = this._resource.requestData;
225         if (!requestData)
226             return false;
227
228         if (this._resource.hasRequestFormParameters())
229             return false;
230
231         return true;
232     }
233
234     _canShowCustomRequestContentView()
235     {
236         return !!(this._customRequestDOMContentViewInitializer || this._customRequestJSONContentViewInitializer);
237     }
238
239     _canShowCustomResponseContentView()
240     {
241         return !!(this._customResponseDOMContentViewInitializer || this._customResponseJSONContentViewInitializer || this._customResponseTextContentViewInitializer);
242     }
243
244     _contentViewForResourceType(type)
245     {
246         switch (type) {
247         case WI.Resource.Type.Document:
248         case WI.Resource.Type.Script:
249         case WI.Resource.Type.StyleSheet:
250             return new WI.TextResourceContentView(this._resource);
251
252         case WI.Resource.Type.Image:
253             return new WI.ImageResourceContentView(this._resource);
254
255         case WI.Resource.Type.Font:
256             return new WI.FontResourceContentView(this._resource);
257
258         case WI.Resource.Type.WebSocket:
259             return new WI.WebSocketContentView(this._resource);
260
261         default:
262             return null;
263         }
264     }
265
266     _pathComponentForContentView(contentView)
267     {
268         switch (contentView) {
269         case this._requestContentView:
270             return this._requestPathComponent;
271
272         case this._customRequestDOMContentView:
273             return this._customRequestDOMPathComponent;
274
275         case this._customRequestJSONContentView:
276             return this._customRequestJSONPathComponent;
277
278         case this._responseContentView:
279             return this._responsePathComponent;
280
281         case this._customResponseDOMContentView:
282             return this._customResponseDOMPathComponent;
283
284         case this._customResponseJSONContentView:
285             return this._customResponseJSONPathComponent;
286
287         case this._customResponseTextContentView:
288             return this._customResponseTextPathComponent;
289         }
290
291         console.error("Unknown contentView", contentView);
292         return null;
293     }
294
295     _identifierForContentView(contentView)
296     {
297         console.assert(contentView);
298
299         switch (contentView) {
300         case this._requestContentView:
301             return ResourceClusterContentView.Identifier.Request;
302
303         case this._customRequestDOMContentView:
304             return ResourceClusterContentView.Identifier.RequestDOM;
305
306         case this._customRequestJSONContentView:
307             return ResourceClusterContentView.Identifier.RequestJSON;
308
309         case this._responseContentView:
310             return ResourceClusterContentView.Identifier.Response;
311
312         case this._customResponseDOMContentView:
313             return ResourceClusterContentView.Identifier.ResponseDOM;
314
315         case this._customResponseJSONContentView:
316             return ResourceClusterContentView.Identifier.ResponseJSON;
317
318         case this._customResponseTextContentView:
319             return ResourceClusterContentView.Identifier.ResponseText;
320         }
321
322         console.error("Unknown contentView", contentView);
323         return null;
324     }
325
326     _showContentViewForIdentifier(identifier)
327     {
328         let contentViewToShow = null;
329
330         // This is expected to fall through all the way to the `default`.
331         switch (identifier) {
332         case ResourceClusterContentView.Identifier.RequestDOM:
333             contentViewToShow = this.customRequestDOMContentView;
334             if (contentViewToShow)
335                 break;
336             // fallthrough
337         case ResourceClusterContentView.Identifier.RequestJSON:
338             contentViewToShow = this.customRequestJSONContentView;
339             if (contentViewToShow)
340                 break;
341             // fallthrough
342         case ResourceClusterContentView.Identifier.Request:
343             contentViewToShow = this.requestContentView;
344             if (contentViewToShow)
345                 break;
346             // fallthrough
347         case ResourceClusterContentView.Identifier.ResponseDOM:
348             contentViewToShow = this.customResponseDOMContentView;
349             if (contentViewToShow)
350                 break;
351             // fallthrough
352         case ResourceClusterContentView.Identifier.ResponseJSON:
353             contentViewToShow = this.customResponseJSONContentView;
354             if (contentViewToShow)
355                 break;
356             // fallthrough
357         case ResourceClusterContentView.Identifier.ResponseText:
358             contentViewToShow = this.customResponseTextContentView;
359             if (contentViewToShow)
360                 break;
361             // fallthrough
362         case ResourceClusterContentView.Identifier.Response:
363         default:
364             contentViewToShow = this.responseContentView;
365             break;
366         }
367
368         console.assert(contentViewToShow);
369
370         this._currentContentViewSetting.value = this._identifierForContentView(contentViewToShow);
371
372         return this.contentViewContainer.showContentView(contentViewToShow);
373     }
374
375     _pathComponentSelected(event)
376     {
377         this._showContentViewForIdentifier(event.data.pathComponent.representedObject);
378     }
379
380     _resourceTypeDidChange(event)
381     {
382         // Since resource views are based on the type, we need to make a new content view and tell the container to replace this
383         // content view with the new one. Make a new ResourceContentView which will use the new resource type to make the correct
384         // concrete ResourceContentView subclass.
385
386         let currentResponseContentView = this._responseContentView;
387         if (!currentResponseContentView)
388             return;
389
390         this._responseContentView = null;
391
392         this.contentViewContainer.replaceContentView(currentResponseContentView, this.responseContentView);
393     }
394
395     _resourceLoadingDidFinish(event)
396     {
397         this._tryEnableCustomResponseContentViews();
398
399         if ("_positionToReveal" in this) {
400             if (this._contentViewContainer.currentContentView === this._responseContentView)
401                 this._responseContentView.revealPosition(this._positionToReveal, this._textRangeToSelect, this._forceUnformatted);
402
403             delete this._positionToReveal;
404             delete this._textRangeToSelect;
405             delete this._forceUnformatted;
406         }
407     }
408
409     _canUseJSONContentViewForContent(content)
410     {
411         return typeof content === "string" && content.isJSON((json) => json && (typeof json === "object" || Array.isArray(json)));
412     }
413
414     _canUseDOMContentViewForContent(content, mimeType)
415     {
416         if (typeof content !== "string")
417             return false;
418
419         switch (mimeType) {
420         case "text/html":
421             return true;
422
423         case "text/xml":
424         case "application/xml":
425         case "application/xhtml+xml":
426         case "image/svg+xml":
427             try {
428                 let dom = (new DOMParser).parseFromString(content, mimeType);
429                 return !dom.querySelector("parsererror");
430             } catch { }
431             return false;
432         }
433
434         return false;
435     }
436
437     _normalizeMIMETypeForDOM(mimeType)
438     {
439         mimeType = parseMIMEType(mimeType).type;
440
441         if (mimeType.endsWith("/html") || mimeType.endsWith("+html"))
442             return "text/html";
443
444         if (mimeType.endsWith("/xml") || mimeType.endsWith("+xml")) {
445             if (mimeType !== "application/xhtml+xml" && mimeType !== "image/svg+xml")
446                 return "application/xml";
447         }
448
449         if (mimeType.endsWith("/xhtml") || mimeType.endsWith("+xhtml"))
450             return "application/xhtml+xml";
451
452         if (mimeType.endsWith("/svg") || mimeType.endsWith("+svg"))
453             return "image/svg+xml";
454
455         return mimeType;
456     }
457
458     _tryEnableCustomRequestContentViews()
459     {
460         let content = this._resource.requestData;
461
462         if (this._canUseJSONContentViewForContent(content)) {
463             this._customRequestJSONContentViewInitializer = () => new WI.LocalJSONContentView(content, this._resource);
464
465             this._customRequestJSONPathComponent = this._createPathComponent({
466                 displayName: WI.UIString("Request (Object Tree)"),
467                 styleClassNames: ["object-icon"],
468                 identifier: ResourceClusterContentView.Identifier.RequestJSON,
469                 previousSibling: this._requestPathComponent,
470                 nextSibling: this._responsePathComponent,
471             });
472
473             this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange);
474             return;
475         }
476
477         let mimeType = this._normalizeMIMETypeForDOM(this._resource.requestDataContentType);
478
479         if (this._canUseDOMContentViewForContent(content, mimeType)) {
480             this._customRequestDOMContentViewInitializer = () => new WI.LocalDOMContentView(content, mimeType, this._resource);
481
482             this._customRequestDOMPathComponent = this._createPathComponent({
483                 displayName: WI.UIString("Request (DOM Tree)"),
484                 styleClassNames: ["dom-document-icon"],
485                 identifier: ResourceClusterContentView.Identifier.RequestDOM,
486                 previousSibling: this._requestPathComponent,
487                 nextSibling: this._responsePathComponent,
488             });
489
490             this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange);
491             return;
492         }
493     }
494
495     _tryEnableCustomResponseContentViews()
496     {
497         if (!this._resource.hasResponse())
498             return;
499
500         // WebSocket resources already use a "custom" response content view.
501         if (this._resource instanceof WI.WebSocketResource)
502             return;
503
504         this._resource.requestContent()
505         .then(({error, content}) => {
506             if (error || typeof content !== "string")
507                 return;
508
509             if (this._canUseJSONContentViewForContent(content)) {
510                 this._customResponseJSONContentViewInitializer = () => new WI.LocalJSONContentView(content, this._resource);
511
512                 this._customResponseJSONPathComponent = this._createPathComponent({
513                     displayName: WI.UIString("Response (Object Tree)"),
514                     styleClassNames: ["object-icon"],
515                     identifier: ResourceClusterContentView.Identifier.ResponseJSON,
516                     previousSibling: this._responsePathComponent,
517                 });
518
519                 this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange);
520                 return;
521             }
522
523             let mimeType = this._normalizeMIMETypeForDOM(this._resource.mimeType);
524
525             if (this._canUseDOMContentViewForContent(content, mimeType)) {
526                 if (mimeType === "image/svg+xml") {
527                     this._customResponseTextContentViewInitializer = () => new WI.TextContentView(content, mimeType, this._resource);
528
529                     this._customResponseTextPathComponent = this._createPathComponent({
530                         displayName: WI.UIString("Response (Text)"),
531                         styleClassNames: ["source-icon"],
532                         identifier: ResourceClusterContentView.Identifier.ResponseText,
533                         previousSibling: this._responsePathComponent,
534                     });
535                 }
536
537                 this._customResponseDOMContentViewInitializer = () => new WI.LocalDOMContentView(content, mimeType, this._resource);
538
539                 this._customResponseDOMPathComponent = this._createPathComponent({
540                     displayName: WI.UIString("Response (DOM Tree)"),
541                     styleClassNames: ["dom-document-icon"],
542                     identifier: ResourceClusterContentView.Identifier.ResponseDOM,
543                     previousSibling: this._customResponseTextPathComponent || this._responsePathComponent,
544                 });
545
546                 this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange);
547                 return;
548             }
549         });
550     }
551 };
552
553 WI.ResourceClusterContentView.ContentViewIdentifierCookieKey = "resource-cluster-content-view-identifier";
554
555 WI.ResourceClusterContentView.Identifier = {
556     Request: "request",
557     RequestDOM: "request-dom",
558     RequestJSON: "request-json",
559     Response: "response",
560     ResponseDOM: "response-dom",
561     ResponseJSON: "response-json",
562     ResponseText: "response-text",
563 };