Web Inspector: Network: add button to show system certificate dialog
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / ResourceSecurityContentView.js
1 /*
2  * Copyright (C) 2018 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.ResourceSecurityContentView = class ResourceSecurityContentView extends WI.ContentView
27 {
28     constructor(resource)
29     {
30         console.assert(resource instanceof WI.Resource);
31
32         super();
33
34         this._resource = resource;
35
36         this._insecureMessageElement = null;
37         this._needsCertificateRefresh = true;
38
39         this._searchQuery = null;
40         this._searchResults = null;
41         this._searchDOMChanges = [];
42         this._searchIndex = -1;
43         this._automaticallyRevealFirstSearchResult = false;
44         this._bouncyHighlightElement = null;
45
46         this.element.classList.add("resource-details", "resource-security");
47     }
48
49     // Protected
50
51     initialLayout()
52     {
53         super.initialLayout();
54
55         this._certificateSection = new WI.ResourceDetailsSection(WI.UIString("Certificate"), "certificate");
56         this.element.appendChild(this._certificateSection.element);
57
58         this._resource.addEventListener(WI.Resource.Event.ResponseReceived, this._handleResourceResponseReceived, this);
59     }
60
61     layout()
62     {
63         super.layout();
64
65         if (!this._resource.loadedSecurely) {
66             if (!this._insecureMessageElement)
67                 this._insecureMessageElement = WI.createMessageTextView(WI.UIString("The resource was requested insecurely."), true);
68             this.element.appendChild(this._insecureMessageElement);
69             return;
70         }
71
72         if (this._needsCertificateRefresh) {
73             this._needsCertificateRefresh = false;
74             this._refreshCetificateSection();
75         }
76     }
77
78     closed()
79     {
80         this._resource.removeEventListener(null, null, this);
81
82         super.closed();
83     }
84
85     get supportsSearch()
86     {
87         return true;
88     }
89
90     get numberOfSearchResults()
91     {
92         return this._searchResults ? this._searchResults.length : null;
93     }
94
95     get hasPerformedSearch()
96     {
97         return this._searchResults !== null;
98     }
99
100     set automaticallyRevealFirstSearchResult(reveal)
101     {
102         this._automaticallyRevealFirstSearchResult = reveal;
103
104         // If we haven't shown a search result yet, reveal one now.
105         if (this._automaticallyRevealFirstSearchResult && this.numberOfSearchResults > 0) {
106             if (this._searchIndex === -1)
107                 this.revealNextSearchResult();
108         }
109     }
110
111     performSearch(query)
112     {
113         if (query === this._searchQuery)
114             return;
115
116         WI.revertDOMChanges(this._searchDOMChanges);
117
118         this._searchQuery = query;
119         this._searchResults = [];
120         this._searchDOMChanges = [];
121         this._searchIndex = -1;
122
123         this._perfomSearchOnKeyValuePairs();
124
125         this.dispatchEventToListeners(WI.ContentView.Event.NumberOfSearchResultsDidChange);
126
127         if (this._automaticallyRevealFirstSearchResult && this._searchResults.length > 0)
128             this.revealNextSearchResult();
129     }
130
131     searchCleared()
132     {
133         WI.revertDOMChanges(this._searchDOMChanges);
134
135         this._searchQuery = null;
136         this._searchResults = null;
137         this._searchDOMChanges = [];
138         this._searchIndex = -1;
139     }
140
141     revealPreviousSearchResult(changeFocus)
142     {
143         if (!this.numberOfSearchResults)
144             return;
145
146         if (this._searchIndex > 0)
147             --this._searchIndex;
148         else
149             this._searchIndex = this._searchResults.length - 1;
150
151         this._revealSearchResult(this._searchIndex, changeFocus);
152     }
153
154     revealNextSearchResult(changeFocus)
155     {
156         if (!this.numberOfSearchResults)
157             return;
158
159         if (this._searchIndex < this._searchResults.length - 1)
160             ++this._searchIndex;
161         else
162             this._searchIndex = 0;
163
164         this._revealSearchResult(this._searchIndex, changeFocus);
165     }
166
167     // Private
168
169     _refreshCetificateSection()
170     {
171         let detailsElement = this._certificateSection.detailsElement;
172         detailsElement.removeChildren();
173
174         let responseSecurity = this._resource.responseSecurity;
175         if (!responseSecurity) {
176             this._certificateSection.markIncompleteSectionWithMessage(WI.UIString("No response security information."));
177             return;
178         }
179
180         let certificate = responseSecurity.certificate;
181         if (!certificate) {
182             this._certificateSection.markIncompleteSectionWithMessage(WI.UIString("No response security certificate."));
183             return;
184         }
185
186         if (WI.NetworkManager.supportsShowCertificate()) {
187             let button = document.createElement("button");
188             button.textContent = WI.UIString("Show full certificate");
189
190             let errorElement = null;
191             button.addEventListener("click", (event) => {
192                 this._resource.showCertificate()
193                 .then(() => {
194                     if (errorElement) {
195                         errorElement.remove();
196                         errorElement = null;
197                     }
198                 })
199                 .catch((error) => {
200                     if (!errorElement)
201                         errorElement = WI.ImageUtilities.useSVGSymbol("Images/Error.svg", "error", error);
202                     button.insertAdjacentElement("afterend", errorElement);
203                 });
204             });
205
206             let pairElement = this._certificateSection.appendKeyValuePair(button);
207             pairElement.classList.add("show-certificate");
208         }
209
210         this._certificateSection.appendKeyValuePair(WI.UIString("Subject"), certificate.subject);
211
212         let appendFormattedDate = (key, timestamp) => {
213             if (isNaN(timestamp))
214                 return;
215
216             let date = new Date(timestamp * 1000);
217
218             let timeElement = document.createElement("time");
219             timeElement.datetime = date.toISOString();
220             timeElement.textContent = date.toLocaleString();
221             this._certificateSection.appendKeyValuePair(key, timeElement);
222
223         };
224         appendFormattedDate(WI.UIString("Valid From"), certificate.validFrom);
225         appendFormattedDate(WI.UIString("Valid Until"), certificate.validUntil);
226
227         let appendList = (key, values, className) => {
228             if (!Array.isArray(values))
229                 return;
230
231             const initialCount = 5;
232             for (let i = 0; i < Math.min(values.length, initialCount); ++i)
233                 this._certificateSection.appendKeyValuePair(key, values[i], className);
234
235             let remaining = values.length - initialCount;
236             if (remaining <= 0)
237                 return;
238
239             let showMoreElement = document.createElement("a");
240             showMoreElement.classList.add("show-more");
241             showMoreElement.textContent = WI.UIString("Show %d More").format(remaining);
242
243             let showMorePair = this._certificateSection.appendKeyValuePair(key, showMoreElement, className);
244
245             showMoreElement.addEventListener("click", (event) => {
246                 showMorePair.remove();
247
248                 for (let i = initialCount; i < values.length; ++i)
249                     this._certificateSection.appendKeyValuePair(key, values[i], className);
250             }, {once: true});
251         };
252         appendList(WI.UIString("DNS"), certificate.dnsNames, "dns-name");
253         appendList(WI.UIString("IP"), certificate.ipAddresses, "ip-address");
254     }
255
256     _perfomSearchOnKeyValuePairs()
257     {
258         let searchRegex = new RegExp(this._searchQuery.escapeForRegExp(), "gi");
259
260         let elements = this.element.querySelectorAll(".key, .value");
261         for (let element of elements) {
262             let matchRanges = [];
263             let text = element.textContent;
264             let match;
265             while (match = searchRegex.exec(text))
266                 matchRanges.push({offset: match.index, length: match[0].length});
267
268             if (matchRanges.length) {
269                 let highlightedNodes = WI.highlightRangesWithStyleClass(element, matchRanges, "search-highlight", this._searchDOMChanges);
270                 this._searchResults = this._searchResults.concat(highlightedNodes);
271             }
272         }
273     }
274
275     _revealSearchResult(index, changeFocus)
276     {
277         let highlightElement = this._searchResults[index];
278         if (!highlightElement)
279             return;
280
281         highlightElement.scrollIntoViewIfNeeded();
282
283         if (!this._bouncyHighlightElement) {
284             this._bouncyHighlightElement = document.createElement("div");
285             this._bouncyHighlightElement.className = "bouncy-highlight";
286             this._bouncyHighlightElement.addEventListener("animationend", (event) => {
287                 this._bouncyHighlightElement.remove();
288             });
289         }
290
291         this._bouncyHighlightElement.remove();
292
293         let computedStyles = window.getComputedStyle(highlightElement);
294         let highlightElementRect = highlightElement.getBoundingClientRect();
295         let contentViewRect = this.element.getBoundingClientRect();
296         let contentViewScrollTop = this.element.scrollTop;
297         let contentViewScrollLeft = this.element.scrollLeft;
298
299         this._bouncyHighlightElement.textContent = highlightElement.textContent;
300         this._bouncyHighlightElement.style.top = (highlightElementRect.top - contentViewRect.top + contentViewScrollTop) + "px";
301         this._bouncyHighlightElement.style.left = (highlightElementRect.left - contentViewRect.left + contentViewScrollLeft) + "px";
302         this._bouncyHighlightElement.style.fontWeight = computedStyles.fontWeight;
303
304         this.element.appendChild(this._bouncyHighlightElement);
305     }
306
307     _handleResourceResponseReceived(event)
308     {
309         this._needsCertificateRefresh = true;
310         this.needsLayout();
311     }
312 };