2
* Copyright (C) 2019 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
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.
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.
26
WI.LocalResourceOverridePopover = class LocalResourceOverridePopover extends WI.Popover
32
this._urlCodeMirror = null;
33
this._isCaseSensitiveCheckbox = null;
34
this._isRegexCheckbox = null;
35
this._mimeTypeCodeMirror = null;
36
this._statusCodeCodeMirror = null;
37
this._statusTextCodeMirror = null;
38
this._headersDataGrid = null;
40
this._serializedDataWhenShown = null;
42
this.windowResizeHandler = this._presentOverTargetElement.bind(this);
49
if (!this._targetElement)
52
let url = this._urlCodeMirror.getValue();
56
let isRegex = this._isRegexCheckbox && this._isRegexCheckbox.checked;
58
const schemes = ["http:", "https:", "file:"];
59
if (!schemes.some((scheme) => url.toLowerCase().startsWith(scheme)))
63
// NOTE: We can allow an empty mimeType / statusCode / statusText to pass
64
// network values through, but lets require them for overrides so that
65
// the popover doesn't have to have an additional state for "pass through".
67
let mimeType = this._mimeTypeCodeMirror.getValue() || this._mimeTypeCodeMirror.getOption("placeholder");
71
let statusCode = parseInt(this._statusCodeCodeMirror.getValue());
72
if (isNaN(statusCode))
73
statusCode = parseInt(this._statusCodeCodeMirror.getOption("placeholder"));
74
if (isNaN(statusCode) || statusCode < 0)
77
let statusText = this._statusTextCodeMirror.getValue() || this._statusTextCodeMirror.getOption("placeholder");
82
for (let node of this._headersDataGrid.children) {
83
let {name, value} = node.data;
86
if (name.toLowerCase() === "content-type")
88
if (name.toLowerCase() === "set-cookie")
90
headers[name] = value;
101
if (this._isCaseSensitiveCheckbox)
102
data.isCaseSensitive = this._isCaseSensitiveCheckbox.checked;
104
if (this._isRegexCheckbox)
105
data.isRegex = this._isRegexCheckbox.checked;
108
let oldSerialized = JSON.stringify(this._serializedDataWhenShown);
109
let newSerialized = JSON.stringify(data);
110
if (oldSerialized === newSerialized)
116
show(localResourceOverride, targetElement, preferredEdges)
118
this._targetElement = targetElement;
119
this._preferredEdges = preferredEdges;
121
let localResource = localResourceOverride ? localResourceOverride.localResource : null;
124
let resourceData = {};
126
data.url = resourceData.url = localResource.url;
127
data.mimeType = resourceData.mimeType = localResource.mimeType;
128
data.statusCode = resourceData.statusCode = String(localResource.statusCode);
129
data.statusText = resourceData.statusText = localResource.statusText;
133
data.url = this._defaultURL();
136
data.mimeType = "text/javascript";
138
if (!data.statusCode || data.statusCode === "NaN") {
139
data.statusCode = "200";
140
resourceData.statusCode = undefined;
143
if (!data.statusText) {
144
data.statusText = WI.HTTPUtilities.statusTextForStatusCode(parseInt(data.statusCode));
145
resourceData.statusText = undefined;
148
let responseHeaders = localResource ? localResource.responseHeaders : {};
150
let popoverContentElement = document.createElement("div");
151
popoverContentElement.className = "local-resource-override-popover-content";
153
let table = popoverContentElement.appendChild(document.createElement("table"));
155
let createRow = (label, id, value, placeholder) => {
156
let row = table.appendChild(document.createElement("tr"));
157
let headerElement = row.appendChild(document.createElement("th"));
158
let dataElement = row.appendChild(document.createElement("td"));
160
let labelElement = headerElement.appendChild(document.createElement("label"));
161
labelElement.textContent = label;
163
let editorElement = dataElement.appendChild(document.createElement("div"));
164
editorElement.classList.add("editor", id);
166
let codeMirror = this._createEditor(editorElement, {value, placeholder});
167
let inputField = codeMirror.getInputField();
168
inputField.id = `local-resource-override-popover-${id}-input-field`;
169
labelElement.setAttribute("for", inputField.id);
171
return {codeMirror, dataElement};
174
let urlRow = createRow(WI.UIString("URL"), "url", resourceData.url || "", data.url);
175
this._urlCodeMirror = urlRow.codeMirror;
177
let updateURLCodeMirrorMode = () => {
178
let isRegex = this._isRegexCheckbox && this._isRegexCheckbox.checked;
180
this._urlCodeMirror.setOption("mode", isRegex ? "text/x-regex" : "text/x-local-override-url");
183
let url = this._urlCodeMirror.getValue();
185
const schemes = ["http:", "https:", "file:"];
186
if (!schemes.some((scheme) => url.toLowerCase().startsWith(scheme)))
187
this._urlCodeMirror.setValue("http://" + url);
192
if (InspectorBackend.hasCommand("Network.addInterception", "caseSensitive")) {
193
let isCaseSensitiveLabel = urlRow.dataElement.appendChild(document.createElement("label"));
194
isCaseSensitiveLabel.className = "is-case-sensitive";
196
this._isCaseSensitiveCheckbox = isCaseSensitiveLabel.appendChild(document.createElement("input"));
197
this._isCaseSensitiveCheckbox.type = "checkbox";
198
this._isCaseSensitiveCheckbox.checked = localResourceOverride ? localResourceOverride.isCaseSensitive : true;
200
isCaseSensitiveLabel.append(WI.UIString("Case Sensitive"));
203
if (InspectorBackend.hasCommand("Network.addInterception", "isRegex")) {
204
let isRegexLabel = urlRow.dataElement.appendChild(document.createElement("label"));
205
isRegexLabel.className = "is-regex";
207
this._isRegexCheckbox = isRegexLabel.appendChild(document.createElement("input"));
208
this._isRegexCheckbox.type = "checkbox";
209
this._isRegexCheckbox.checked = localResourceOverride ? localResourceOverride.isRegex : false;
210
this._isRegexCheckbox.addEventListener("change", (event) => {
211
updateURLCodeMirrorMode();
214
isRegexLabel.append(WI.UIString("Regular Expression"));
217
let mimeTypeRow = createRow(WI.UIString("MIME Type"), "mime", resourceData.mimeType || "", data.mimeType);
218
this._mimeTypeCodeMirror = mimeTypeRow.codeMirror;
220
let statusCodeRow = createRow(WI.UIString("Status"), "status", resourceData.statusCode || "", data.statusCode);
221
this._statusCodeCodeMirror = statusCodeRow.codeMirror;
223
let statusTextEditorElement = statusCodeRow.dataElement.appendChild(document.createElement("div"));
224
statusTextEditorElement.className = "editor status-text";
225
this._statusTextCodeMirror = this._createEditor(statusTextEditorElement, {value: resourceData.statusText || "", placeholder: data.statusText});
227
let editCallback = () => {};
228
let deleteCallback = (node) => {
229
if (node === contentTypeDataGridNode)
232
let siblingToSelect = node.nextSibling || node.previousSibling;
233
this._headersDataGrid.removeChild(node);
235
siblingToSelect.select();
237
this._headersDataGrid.updateLayoutIfNeeded();
243
title: WI.UIString("Name"),
247
title: WI.UIString("Value"),
251
this._headersDataGrid = new WI.DataGrid(columns, {editCallback, deleteCallback});
252
this._headersDataGrid.inline = true;
253
this._headersDataGrid.variableHeightRows = true;
254
this._headersDataGrid.copyTextDelimiter = ": ";
256
let addDataGridNodeForHeader = (name, value, options = {}) => {
257
let node = new WI.DataGridNode({name, value}, options);
258
this._headersDataGrid.appendChild(node);
262
let contentTypeDataGridNode = addDataGridNodeForHeader("Content-Type", data.mimeType, {selectable: false, editable: false, classNames: ["header-content-type"]});
264
for (let name in responseHeaders) {
265
if (name.toLowerCase() === "content-type")
267
if (name.toLowerCase() === "set-cookie")
269
addDataGridNodeForHeader(name, responseHeaders[name]);
272
let headersRow = table.appendChild(document.createElement("tr"));
273
let headersHeader = headersRow.appendChild(document.createElement("th"));
274
let headersData = headersRow.appendChild(document.createElement("td"));
275
let headersLabel = headersHeader.appendChild(document.createElement("label"));
276
headersLabel.textContent = WI.UIString("Headers");
277
headersData.appendChild(this._headersDataGrid.element);
278
this._headersDataGrid.updateLayoutIfNeeded();
280
let addHeaderButton = headersData.appendChild(document.createElement("button"));
281
addHeaderButton.className = "add-header";
282
addHeaderButton.textContent = WI.UIString("Add Header");
283
addHeaderButton.addEventListener("click", (event) => {
284
let newNode = new WI.DataGridNode({name: "Header", value: "value"});
285
this._headersDataGrid.appendChild(newNode);
286
this._headersDataGrid.updateLayoutIfNeeded();
288
this._headersDataGrid.startEditingNode(newNode);
291
headersData.appendChild(WI.createReferencePageLink("local-overrides", "configuring-local-overrides"));
293
let incrementStatusCode = () => {
294
let x = parseInt(this._statusCodeCodeMirror.getValue());
296
x = parseInt(this._statusCodeCodeMirror.getOption("placeholder"));
297
if (isNaN(x) || x >= 999)
300
if (WI.modifierKeys.shiftKey) {
301
// 200 => 300 and 211 => 300
302
x = (x - (x % 100)) + 100;
309
this._statusCodeCodeMirror.setValue(`${x}`);
310
this._statusCodeCodeMirror.setCursor(this._statusCodeCodeMirror.lineCount(), 0);
313
let decrementStatusCode = () => {
314
let x = parseInt(this._statusCodeCodeMirror.getValue());
316
x = parseInt(this._statusCodeCodeMirror.getOption("placeholder"));
317
if (isNaN(x) || x <= 0)
320
if (WI.modifierKeys.shiftKey) {
321
// 311 => 300 and 300 => 200
332
this._statusCodeCodeMirror.setValue(`${x}`);
333
this._statusCodeCodeMirror.setCursor(this._statusCodeCodeMirror.lineCount(), 0);
336
this._statusCodeCodeMirror.addKeyMap({
337
"Up": incrementStatusCode,
338
"Shift-Up": incrementStatusCode,
339
"Down": decrementStatusCode,
340
"Shift-Down": decrementStatusCode,
343
// Update statusText when statusCode changes.
344
this._statusCodeCodeMirror.on("change", (cm) => {
345
let statusCode = parseInt(cm.getValue());
346
if (isNaN(statusCode)) {
347
this._statusTextCodeMirror.setValue("");
351
let statusText = WI.HTTPUtilities.statusTextForStatusCode(statusCode);
352
this._statusTextCodeMirror.setValue(statusText);
355
// Update mimeType when URL gets a file extension.
356
this._urlCodeMirror.on("change", (cm) => {
357
if (this._isRegexCheckbox && this._isRegexCheckbox.checked)
360
let extension = WI.fileExtensionForURL(cm.getValue());
364
let mimeType = WI.mimeTypeForFileExtension(extension);
368
this._mimeTypeCodeMirror.setValue(mimeType);
369
contentTypeDataGridNode.data = {name: "Content-Type", value: mimeType};
372
// Update Content-Type header when mimeType changes.
373
this._mimeTypeCodeMirror.on("change", (cm) => {
374
let mimeType = cm.getValue() || cm.getOption("placeholder");
375
contentTypeDataGridNode.data = {name: "Content-Type", value: mimeType};
378
updateURLCodeMirrorMode();
380
this._serializedDataWhenShown = this.serializedData;
382
this.content = popoverContentElement;
383
this._presentOverTargetElement();
385
// CodeMirror needs a refresh after the popover displays, to layout, otherwise it doesn't appear.
387
this._urlCodeMirror.refresh();
388
this._mimeTypeCodeMirror.refresh();
389
this._statusCodeCodeMirror.refresh();
390
this._statusTextCodeMirror.refresh();
392
this._urlCodeMirror.focus();
393
this._urlCodeMirror.setCursor(this._urlCodeMirror.lineCount(), 0);
401
_createEditor(element, options = {})
403
let codeMirror = WI.CodeMirrorEditor.create(element, {
404
extraKeys: {"Tab": false, "Shift-Tab": false},
408
scrollbarStyle: null,
412
codeMirror.addKeyMap({
413
"Enter": () => { this.dismiss(); },
414
"Shift-Enter": () => { this.dismiss(); },
415
"Esc": () => { this.dismiss(); },
423
// We avoid just doing "http://example.com/" here because users can
424
// accidentally override the main resource, even though the popover
425
// typically prevents no-edit cases.
426
let mainFrame = WI.networkManager.mainFrame;
427
if (mainFrame && mainFrame.securityOrigin.startsWith("http"))
428
return mainFrame.securityOrigin + "/path";
433
_presentOverTargetElement()
435
if (!this._targetElement)
438
let targetFrame = WI.Rect.rectFromClientRect(this._targetElement.getBoundingClientRect());
439
this.present(targetFrame.pad(2), this._preferredEdges);