~mmach/netext73/webkit2gtk

« back to all changes in this revision

Viewing changes to Source/WebInspectorUI/UserInterface/Views/LocalResourceOverridePopover.js

  • Committer: mmach
  • Date: 2023-06-16 17:21:37 UTC
  • Revision ID: netbit73@gmail.com-20230616172137-2rqx6yr96ga9g3kp
1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/*
 
2
 * Copyright (C) 2019 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.LocalResourceOverridePopover = class LocalResourceOverridePopover extends WI.Popover
 
27
{
 
28
    constructor(delegate)
 
29
    {
 
30
        super(delegate);
 
31
 
 
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;
 
39
 
 
40
        this._serializedDataWhenShown = null;
 
41
 
 
42
        this.windowResizeHandler = this._presentOverTargetElement.bind(this);
 
43
    }
 
44
 
 
45
    // Public
 
46
 
 
47
    get serializedData()
 
48
    {
 
49
        if (!this._targetElement)
 
50
            return null;
 
51
 
 
52
        let url = this._urlCodeMirror.getValue();
 
53
        if (!url)
 
54
            return null;
 
55
 
 
56
        let isRegex = this._isRegexCheckbox && this._isRegexCheckbox.checked;
 
57
        if (!isRegex) {
 
58
            const schemes = ["http:", "https:", "file:"];
 
59
            if (!schemes.some((scheme) => url.toLowerCase().startsWith(scheme)))
 
60
                return null;
 
61
        }
 
62
 
 
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".
 
66
 
 
67
        let mimeType = this._mimeTypeCodeMirror.getValue() || this._mimeTypeCodeMirror.getOption("placeholder");
 
68
        if (!mimeType)
 
69
            return null;
 
70
 
 
71
        let statusCode = parseInt(this._statusCodeCodeMirror.getValue());
 
72
        if (isNaN(statusCode))
 
73
            statusCode = parseInt(this._statusCodeCodeMirror.getOption("placeholder"));
 
74
        if (isNaN(statusCode) || statusCode < 0)
 
75
            return null;
 
76
 
 
77
        let statusText = this._statusTextCodeMirror.getValue() || this._statusTextCodeMirror.getOption("placeholder");
 
78
        if (!statusText)
 
79
            return null;
 
80
 
 
81
        let headers = {};
 
82
        for (let node of this._headersDataGrid.children) {
 
83
            let {name, value} = node.data;
 
84
            if (!name || !value)
 
85
                continue;
 
86
            if (name.toLowerCase() === "content-type")
 
87
                continue;
 
88
            if (name.toLowerCase() === "set-cookie")
 
89
                continue;
 
90
            headers[name] = value;
 
91
        }
 
92
 
 
93
        let data = {
 
94
            url,
 
95
            mimeType,
 
96
            statusCode,
 
97
            statusText,
 
98
            headers,
 
99
        };
 
100
 
 
101
        if (this._isCaseSensitiveCheckbox)
 
102
            data.isCaseSensitive = this._isCaseSensitiveCheckbox.checked;
 
103
 
 
104
        if (this._isRegexCheckbox)
 
105
            data.isRegex = this._isRegexCheckbox.checked;
 
106
 
 
107
        // No change.
 
108
        let oldSerialized = JSON.stringify(this._serializedDataWhenShown);
 
109
        let newSerialized = JSON.stringify(data);
 
110
        if (oldSerialized === newSerialized)
 
111
            return null;
 
112
 
 
113
        return data;
 
114
    }
 
115
 
 
116
    show(localResourceOverride, targetElement, preferredEdges)
 
117
    {
 
118
        this._targetElement = targetElement;
 
119
        this._preferredEdges = preferredEdges;
 
120
 
 
121
        let localResource = localResourceOverride ? localResourceOverride.localResource : null;
 
122
 
 
123
        let data = {};
 
124
        let resourceData = {};
 
125
        if (localResource) {
 
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;
 
130
        }
 
131
 
 
132
        if (!data.url)
 
133
            data.url = this._defaultURL();
 
134
 
 
135
        if (!data.mimeType)
 
136
            data.mimeType = "text/javascript";
 
137
 
 
138
        if (!data.statusCode || data.statusCode === "NaN") {
 
139
            data.statusCode = "200";
 
140
            resourceData.statusCode = undefined;
 
141
        }
 
142
 
 
143
        if (!data.statusText) {
 
144
            data.statusText = WI.HTTPUtilities.statusTextForStatusCode(parseInt(data.statusCode));
 
145
            resourceData.statusText = undefined;
 
146
        }
 
147
 
 
148
        let responseHeaders = localResource ? localResource.responseHeaders : {};
 
149
 
 
150
        let popoverContentElement = document.createElement("div");
 
151
        popoverContentElement.className = "local-resource-override-popover-content";
 
152
 
 
153
        let table = popoverContentElement.appendChild(document.createElement("table"));
 
154
 
 
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"));
 
159
 
 
160
            let labelElement = headerElement.appendChild(document.createElement("label"));
 
161
            labelElement.textContent = label;
 
162
 
 
163
            let editorElement = dataElement.appendChild(document.createElement("div"));
 
164
            editorElement.classList.add("editor", id);
 
165
 
 
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);
 
170
 
 
171
            return {codeMirror, dataElement};
 
172
        };
 
173
 
 
174
        let urlRow = createRow(WI.UIString("URL"), "url", resourceData.url || "", data.url);
 
175
        this._urlCodeMirror = urlRow.codeMirror;
 
176
 
 
177
        let updateURLCodeMirrorMode = () => {
 
178
            let isRegex = this._isRegexCheckbox && this._isRegexCheckbox.checked;
 
179
 
 
180
            this._urlCodeMirror.setOption("mode", isRegex ? "text/x-regex" : "text/x-local-override-url");
 
181
 
 
182
            if (!isRegex) {
 
183
                let url = this._urlCodeMirror.getValue();
 
184
                if (url) {
 
185
                    const schemes = ["http:", "https:", "file:"];
 
186
                    if (!schemes.some((scheme) => url.toLowerCase().startsWith(scheme)))
 
187
                        this._urlCodeMirror.setValue("http://" + url);
 
188
                }
 
189
            }
 
190
        };
 
191
 
 
192
        if (InspectorBackend.hasCommand("Network.addInterception", "caseSensitive")) {
 
193
            let isCaseSensitiveLabel = urlRow.dataElement.appendChild(document.createElement("label"));
 
194
            isCaseSensitiveLabel.className = "is-case-sensitive";
 
195
 
 
196
            this._isCaseSensitiveCheckbox = isCaseSensitiveLabel.appendChild(document.createElement("input"));
 
197
            this._isCaseSensitiveCheckbox.type = "checkbox";
 
198
            this._isCaseSensitiveCheckbox.checked = localResourceOverride ? localResourceOverride.isCaseSensitive : true;
 
199
 
 
200
            isCaseSensitiveLabel.append(WI.UIString("Case Sensitive"));
 
201
        }
 
202
 
 
203
        if (InspectorBackend.hasCommand("Network.addInterception", "isRegex")) {
 
204
            let isRegexLabel = urlRow.dataElement.appendChild(document.createElement("label"));
 
205
            isRegexLabel.className = "is-regex";
 
206
 
 
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();
 
212
            });
 
213
 
 
214
            isRegexLabel.append(WI.UIString("Regular Expression"));
 
215
        }
 
216
 
 
217
        let mimeTypeRow = createRow(WI.UIString("MIME Type"), "mime", resourceData.mimeType || "", data.mimeType);
 
218
        this._mimeTypeCodeMirror = mimeTypeRow.codeMirror;
 
219
 
 
220
        let statusCodeRow = createRow(WI.UIString("Status"), "status", resourceData.statusCode || "", data.statusCode);
 
221
        this._statusCodeCodeMirror = statusCodeRow.codeMirror;
 
222
 
 
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});
 
226
 
 
227
        let editCallback = () => {};
 
228
        let deleteCallback = (node) => {
 
229
            if (node === contentTypeDataGridNode)
 
230
                return;
 
231
 
 
232
            let siblingToSelect = node.nextSibling || node.previousSibling;
 
233
            this._headersDataGrid.removeChild(node);
 
234
            if (siblingToSelect)
 
235
                siblingToSelect.select();
 
236
 
 
237
            this._headersDataGrid.updateLayoutIfNeeded();
 
238
            this.update();
 
239
        };
 
240
 
 
241
        let columns = {
 
242
            name: {
 
243
                title: WI.UIString("Name"),
 
244
                width: "30%",
 
245
            },
 
246
            value: {
 
247
                title: WI.UIString("Value"),
 
248
            },
 
249
        };
 
250
 
 
251
        this._headersDataGrid = new WI.DataGrid(columns, {editCallback, deleteCallback});
 
252
        this._headersDataGrid.inline = true;
 
253
        this._headersDataGrid.variableHeightRows = true;
 
254
        this._headersDataGrid.copyTextDelimiter = ": ";
 
255
 
 
256
        let addDataGridNodeForHeader = (name, value, options = {}) => {
 
257
            let node = new WI.DataGridNode({name, value}, options);
 
258
            this._headersDataGrid.appendChild(node);
 
259
            return node;
 
260
        };
 
261
 
 
262
        let contentTypeDataGridNode = addDataGridNodeForHeader("Content-Type", data.mimeType, {selectable: false, editable: false, classNames: ["header-content-type"]});
 
263
 
 
264
        for (let name in responseHeaders) {
 
265
            if (name.toLowerCase() === "content-type")
 
266
                continue;
 
267
            if (name.toLowerCase() === "set-cookie")
 
268
                continue;
 
269
            addDataGridNodeForHeader(name, responseHeaders[name]);
 
270
        }
 
271
 
 
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();
 
279
 
 
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();
 
287
            this.update();
 
288
            this._headersDataGrid.startEditingNode(newNode);
 
289
        });
 
290
 
 
291
        headersData.appendChild(WI.createReferencePageLink("local-overrides", "configuring-local-overrides"));
 
292
 
 
293
        let incrementStatusCode = () => {
 
294
            let x = parseInt(this._statusCodeCodeMirror.getValue());
 
295
            if (isNaN(x))
 
296
                x = parseInt(this._statusCodeCodeMirror.getOption("placeholder"));
 
297
            if (isNaN(x) || x >= 999)
 
298
                return;
 
299
 
 
300
            if (WI.modifierKeys.shiftKey) {
 
301
                // 200 => 300 and 211 => 300
 
302
                x = (x - (x % 100)) + 100;
 
303
            } else
 
304
                x += 1;
 
305
 
 
306
            if (x > 999)
 
307
                x = 999;
 
308
 
 
309
            this._statusCodeCodeMirror.setValue(`${x}`);
 
310
            this._statusCodeCodeMirror.setCursor(this._statusCodeCodeMirror.lineCount(), 0);
 
311
        };
 
312
 
 
313
        let decrementStatusCode = () => {
 
314
            let x = parseInt(this._statusCodeCodeMirror.getValue());
 
315
            if (isNaN(x))
 
316
                x = parseInt(this._statusCodeCodeMirror.getOption("placeholder"));
 
317
            if (isNaN(x) || x <= 0)
 
318
                return;
 
319
 
 
320
            if (WI.modifierKeys.shiftKey) {
 
321
                // 311 => 300 and 300 => 200
 
322
                let original = x;
 
323
                x = (x - (x % 100));
 
324
                if (original === x)
 
325
                    x -= 100;
 
326
            } else
 
327
                x -= 1;
 
328
 
 
329
            if (x < 0)
 
330
                x = 0;
 
331
 
 
332
            this._statusCodeCodeMirror.setValue(`${x}`);
 
333
            this._statusCodeCodeMirror.setCursor(this._statusCodeCodeMirror.lineCount(), 0);
 
334
        };
 
335
 
 
336
        this._statusCodeCodeMirror.addKeyMap({
 
337
            "Up": incrementStatusCode,
 
338
            "Shift-Up": incrementStatusCode,
 
339
            "Down": decrementStatusCode,
 
340
            "Shift-Down": decrementStatusCode,
 
341
        });
 
342
 
 
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("");
 
348
                return;
 
349
            }
 
350
 
 
351
            let statusText = WI.HTTPUtilities.statusTextForStatusCode(statusCode);
 
352
            this._statusTextCodeMirror.setValue(statusText);
 
353
        });
 
354
 
 
355
        // Update mimeType when URL gets a file extension.
 
356
        this._urlCodeMirror.on("change", (cm) => {
 
357
            if (this._isRegexCheckbox && this._isRegexCheckbox.checked)
 
358
                return;
 
359
 
 
360
            let extension = WI.fileExtensionForURL(cm.getValue());
 
361
            if (!extension)
 
362
                return;
 
363
 
 
364
            let mimeType = WI.mimeTypeForFileExtension(extension);
 
365
            if (!mimeType)
 
366
                return;
 
367
 
 
368
            this._mimeTypeCodeMirror.setValue(mimeType);
 
369
            contentTypeDataGridNode.data = {name: "Content-Type", value: mimeType};
 
370
        });
 
371
 
 
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};
 
376
        });
 
377
 
 
378
        updateURLCodeMirrorMode();
 
379
 
 
380
        this._serializedDataWhenShown = this.serializedData;
 
381
 
 
382
        this.content = popoverContentElement;
 
383
        this._presentOverTargetElement();
 
384
 
 
385
        // CodeMirror needs a refresh after the popover displays, to layout, otherwise it doesn't appear.
 
386
        setTimeout(() => {
 
387
            this._urlCodeMirror.refresh();
 
388
            this._mimeTypeCodeMirror.refresh();
 
389
            this._statusCodeCodeMirror.refresh();
 
390
            this._statusTextCodeMirror.refresh();
 
391
 
 
392
            this._urlCodeMirror.focus();
 
393
            this._urlCodeMirror.setCursor(this._urlCodeMirror.lineCount(), 0);
 
394
 
 
395
            this.update();
 
396
        });
 
397
    }
 
398
 
 
399
    // Private
 
400
 
 
401
    _createEditor(element, options = {})
 
402
    {
 
403
        let codeMirror = WI.CodeMirrorEditor.create(element, {
 
404
            extraKeys: {"Tab": false, "Shift-Tab": false},
 
405
            lineWrapping: false,
 
406
            mode: "text/plain",
 
407
            matchBrackets: true,
 
408
            scrollbarStyle: null,
 
409
            ...options,
 
410
        });
 
411
 
 
412
        codeMirror.addKeyMap({
 
413
            "Enter": () => { this.dismiss(); },
 
414
            "Shift-Enter": () => { this.dismiss(); },
 
415
            "Esc": () => { this.dismiss(); },
 
416
        });
 
417
 
 
418
        return codeMirror;
 
419
    }
 
420
 
 
421
    _defaultURL()
 
422
    {
 
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";
 
429
 
 
430
        return "https://";
 
431
    }
 
432
 
 
433
    _presentOverTargetElement()
 
434
    {
 
435
        if (!this._targetElement)
 
436
            return;
 
437
 
 
438
        let targetFrame = WI.Rect.rectFromClientRect(this._targetElement.getBoundingClientRect());
 
439
        this.present(targetFrame.pad(2), this._preferredEdges);
 
440
    }
 
441
};