Web Inspector: Local Resource Overrides: allow substitution based on a url pattern
authordrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 19 Nov 2019 01:30:51 +0000 (01:30 +0000)
committerdrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 19 Nov 2019 01:30:51 +0000 (01:30 +0000)
https://bugs.webkit.org/show_bug.cgi?id=202375

Reviewed by Brian Burg.

Source/JavaScriptCore:

Often, websites will load content with a timestamp-based URL query parameter for
cache-busting or logging purposes. If a developer is trying to override these resources (or
is trying to have an existing override also match these resources), they'd need to edit the
local override's URL to match in addition to editing the resource that loads it (e.g. change
the <script> in an HTML file), which can sometimes be tricky of the content is dynamically
loaded (e.g. an XHR with a non-hardcoded URL).

Allowing for local overrides to be set using a regular expression pattern would help resolve
this limitation.

* inspector/protocol/Network.json:
Add `isRegex` parameter to `Network.addInterception` and `Network.removeInterception`.

Source/WebCore:

Test: http/tests/inspector/network/local-resource-override-isRegex.html

Often, websites will load content with a timestamp-based URL query parameter for
cache-busting or logging purposes. If a developer is trying to override these resources (or
is trying to have an existing override also match these resources), they'd need to edit the
local override's URL to match in addition to editing the resource that loads it (e.g. change
the <script> in an HTML file), which can sometimes be tricky of the content is dynamically
loaded (e.g. an XHR with a non-hardcoded URL).

Allowing for local overrides to be set using a regular expression pattern would help resolve
this limitation.

* inspector/agents/InspectorNetworkAgent.h:
(WebCore::InspectorNetworkAgent::Intercept): Added.
(WebCore::InspectorNetworkAgent::Intercept::operator== const): Added.
* inspector/agents/InspectorNetworkAgent.cpp:
(WebCore::InspectorNetworkAgent::disable):
(WebCore::InspectorNetworkAgent::shouldIntercept): Added.
(WebCore::InspectorNetworkAgent::addInterception):
(WebCore::InspectorNetworkAgent::removeInterception):
(WebCore::InspectorNetworkAgent::willInterceptRequest):
(WebCore::InspectorNetworkAgent::shouldInterceptResponse):

Source/WebInspectorUI:

Often, websites will load content with a timestamp-based URL query parameter for
cache-busting or logging purposes. If a developer is trying to override these resources (or
is trying to have an existing override also match these resources), they'd need to edit the
local override's URL to match in addition to editing the resource that loads it (e.g. change
the <script> in an HTML file), which can sometimes be tricky of the content is dynamically
loaded (e.g. an XHR with a non-hardcoded URL).

Allowing for local overrides to be set using a regular expression pattern would help resolve
this limitation.

* UserInterface/Models/LocalResourceOverride.js:
(WI.LocalResourceOverride):
(WI.LocalResourceOverride.create):
(WI.LocalResourceOverride.fromJSON):
(WI.LocalResourceOverride.prototype.toJSON):
(WI.LocalResourceOverride.prototype.get isCaseSensitive): Added.
(WI.LocalResourceOverride.prototype.get isRegex): Added.
(WI.LocalResourceOverride.prototype.matches): Added.
(WI.LocalResourceOverride.prototype.saveIdentityToCookie):

* UserInterface/Controllers/NetworkManager.js:
(WI.NetworkManager):
(WI.NetworkManager.prototype.initializeTarget):
(WI.NetworkManager.prototype.get localResourceOverrides):
(WI.NetworkManager.prototype.addLocalResourceOverride):
(WI.NetworkManager.prototype.removeLocalResourceOverride):
(WI.NetworkManager.prototype.localResourceOverrideForURL):
(WI.NetworkManager.prototype.responseIntercepted):

* UserInterface/Views/LocalResourceOverridePopover.js:
(WI.LocalResourceOverridePopover):
(WI.LocalResourceOverridePopover.prototype.get serializedData):
(WI.LocalResourceOverridePopover.prototype.show):
(WI.LocalResourceOverridePopover.prototype._createEditor):

* UserInterface/Views/LocalResourceOverrideTreeElement.js:
(WI.LocalResourceOverrideTreeElement.prototype.get mainTitleText): Added.
(WI.LocalResourceOverrideTreeElement.prototype.onattach):
(WI.LocalResourceOverrideTreeElement.prototype.ondetach):
(WI.LocalResourceOverrideTreeElement.prototype.populateContextMenu):
(WI.LocalResourceOverrideTreeElement.prototype.willDismissPopover):
* UserInterface/Views/SourcesNavigationSidebarPanel.js:
(WI.SourcesNavigationSidebarPanel.prototype._willDismissLocalOverridePopover):

LayoutTests:

* http/tests/inspector/network/local-resource-override-basic.html:
* http/tests/inspector/network/local-resource-override-basic-expected.txt:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@252614 268f45cc-cd09-0410-ab3c-d52691b4dbfc

16 files changed:
LayoutTests/ChangeLog
LayoutTests/http/tests/inspector/network/local-resource-override-basic-expected.txt
LayoutTests/http/tests/inspector/network/local-resource-override-basic.html
Source/JavaScriptCore/ChangeLog
Source/JavaScriptCore/inspector/protocol/Network.json
Source/WebCore/ChangeLog
Source/WebCore/inspector/agents/InspectorNetworkAgent.cpp
Source/WebCore/inspector/agents/InspectorNetworkAgent.h
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Controllers/NetworkManager.js
Source/WebInspectorUI/UserInterface/Models/LocalResourceOverride.js
Source/WebInspectorUI/UserInterface/Views/LocalResourceOverridePopover.css
Source/WebInspectorUI/UserInterface/Views/LocalResourceOverridePopover.js
Source/WebInspectorUI/UserInterface/Views/LocalResourceOverrideTreeElement.js
Source/WebInspectorUI/UserInterface/Views/SourcesNavigationSidebarPanel.js

index 1c3a98c..91d8eb6 100644 (file)
@@ -1,3 +1,13 @@
+2019-11-18  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Local Resource Overrides: allow substitution based on a url pattern
+        https://bugs.webkit.org/show_bug.cgi?id=202375
+
+        Reviewed by Brian Burg.
+
+        * http/tests/inspector/network/local-resource-override-basic.html:
+        * http/tests/inspector/network/local-resource-override-basic-expected.txt:
+
 2019-11-18  Megan Gardner  <megan_gardner@apple.com>
 
         Update dismiss-picker-using-keyboard.html test to work on iPad correctly
index d8978e9..1a9e687 100644 (file)
@@ -88,6 +88,45 @@ Response Headers:
   X-Expected: PASS
 Content: PASS
 
+-- Running test case: LocalResourceOverride.URL.CaseSensitive
+Creating Local Resource Override for: http://127.0.0.1:8000/inspector/network/resources/override.txt?case=sensitive
+Triggering load...
+Resource Loaded:
+URL: http://127.0.0.1:8000/inspector/network/resources/override.txt?CaSe=SeNsItIvE
+MIME Type: text/plain
+Status: 200 OK
+Response Source: Symbol(inspector-override)
+Response Headers:
+  Content-Type: text/plain
+  X-Expected: PASS
+Content: PASS
+
+-- Running test case: LocalResourceOverride.URL.IsRegex
+Creating Local Resource Override for: \/override\.txt\?t=\d+
+Triggering load...
+Resource Loaded:
+URL: http://127.0.0.1:8000/inspector/network/resources/override.txt?t=123456789
+MIME Type: text/plain
+Status: 200 OK
+Response Source: Symbol(inspector-override)
+Response Headers:
+  Content-Type: text/plain
+  X-Expected: PASS
+Content: PASS
+
+-- Running test case: LocalResourceOverride.URL.IsCaseSensitiveRegex
+Creating Local Resource Override for: \/OvErRiDe\.TxT\?t=\d+
+Triggering load...
+Resource Loaded:
+URL: http://127.0.0.1:8000/inspector/network/resources/override.txt?t=123456789
+MIME Type: text/plain
+Status: 200 OK
+Response Source: Symbol(inspector-override)
+Response Headers:
+  Content-Type: text/plain
+  X-Expected: PASS
+Content: PASS
+
 -- Running test case: LocalResourceOverride.404
 Creating Local Resource Override for: http://127.0.0.1:8000/inspector/network/resources/override.txt
 Triggering load...
index 708e997..0ce0c4b 100644 (file)
@@ -165,6 +165,55 @@ function test()
     });
 
     addTestCase({
+        name: "LocalResourceOverride.URL.CaseSensitive",
+        description: "Test override for a load with a fragment.",
+        expression: `triggerOverrideLoad("?CaSe=SeNsItIvE")`,
+        overrides: [{
+            url: "http://127.0.0.1:8000/inspector/network/resources/override.txt?case=sensitive",
+            mimeType: "text/plain",
+            content: "PASS",
+            base64Encoded: false,
+            statusCode: 200,
+            statusText: "OK",
+            headers: {"X-Expected": "PASS"},
+            isCaseSensitive: false,
+        }]
+    });
+
+    addTestCase({
+        name: "LocalResourceOverride.URL.IsRegex",
+        description: "Test override for a load with a fragment.",
+        expression: `triggerOverrideLoad("?t=123456789")`,
+        overrides: [{
+            url: "\\/override\\.txt\\?t=\\d+",
+            mimeType: "text/plain",
+            content: "PASS",
+            base64Encoded: false,
+            statusCode: 200,
+            statusText: "OK",
+            headers: {"X-Expected": "PASS"},
+            isRegex: true,
+        }]
+    });
+
+    addTestCase({
+        name: "LocalResourceOverride.URL.IsCaseSensitiveRegex",
+        description: "Test override for a load with a fragment.",
+        expression: `triggerOverrideLoad("?t=123456789")`,
+        overrides: [{
+            url: "\\/OvErRiDe\\.TxT\\?t=\\d+",
+            mimeType: "text/plain",
+            content: "PASS",
+            base64Encoded: false,
+            statusCode: 200,
+            statusText: "OK",
+            headers: {"X-Expected": "PASS"},
+            isCaseSensitive: false,
+            isRegex: true,
+        }]
+    });
+
+    addTestCase({
         name: "LocalResourceOverride.404",
         description: "Test for a 404 override.",
         expression: `triggerOverrideLoad()`,
index 9c1558c..fed77c2 100644 (file)
@@ -1,3 +1,23 @@
+2019-11-18  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Local Resource Overrides: allow substitution based on a url pattern
+        https://bugs.webkit.org/show_bug.cgi?id=202375
+
+        Reviewed by Brian Burg.
+
+        Often, websites will load content with a timestamp-based URL query parameter for
+        cache-busting or logging purposes. If a developer is trying to override these resources (or
+        is trying to have an existing override also match these resources), they'd need to edit the
+        local override's URL to match in addition to editing the resource that loads it (e.g. change
+        the <script> in an HTML file), which can sometimes be tricky of the content is dynamically
+        loaded (e.g. an XHR with a non-hardcoded URL).
+
+        Allowing for local overrides to be set using a regular expression pattern would help resolve
+        this limitation.
+
+        * inspector/protocol/Network.json:
+        Add `isRegex` parameter to `Network.addInterception` and `Network.removeInterception`.
+
 2019-11-18  Keith Rollin  <krollin@apple.com>
 
         Move jsc from Resources to Helpers
index 546bbfd..658f14f 100644 (file)
             "description": "Add an interception.",
             "parameters": [
                 { "name": "url", "type": "string" },
+                { "name": "caseSensitive", "type": "boolean", "optional": true, "description": "If false, ignores letter casing of `url` parameter." },
+                { "name": "isRegex", "type": "boolean", "optional": true, "description": "If true, treats `url` parameter as a regular expression." },
                 { "name": "stage", "$ref": "NetworkStage", "optional": true, "description": "If not present this applies to all network stages." }
             ]
         },
             "description": "Remove an interception.",
             "parameters": [
                 { "name": "url", "type": "string" },
+                { "name": "caseSensitive", "type": "boolean", "optional": true, "description": "If false, ignores letter casing of `url` parameter." },
+                { "name": "isRegex", "type": "boolean", "optional": true, "description": "If true, treats `url` parameter as a regular expression." },
                 { "name": "stage", "$ref": "NetworkStage", "optional": true, "description": "If not present this applies to all network stages." }
             ]
         },
index 960be01..283e155 100644 (file)
@@ -1,3 +1,33 @@
+2019-11-18  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Local Resource Overrides: allow substitution based on a url pattern
+        https://bugs.webkit.org/show_bug.cgi?id=202375
+
+        Reviewed by Brian Burg.
+
+        Test: http/tests/inspector/network/local-resource-override-isRegex.html
+
+        Often, websites will load content with a timestamp-based URL query parameter for
+        cache-busting or logging purposes. If a developer is trying to override these resources (or
+        is trying to have an existing override also match these resources), they'd need to edit the
+        local override's URL to match in addition to editing the resource that loads it (e.g. change
+        the <script> in an HTML file), which can sometimes be tricky of the content is dynamically
+        loaded (e.g. an XHR with a non-hardcoded URL).
+
+        Allowing for local overrides to be set using a regular expression pattern would help resolve
+        this limitation.
+
+        * inspector/agents/InspectorNetworkAgent.h:
+        (WebCore::InspectorNetworkAgent::Intercept): Added.
+        (WebCore::InspectorNetworkAgent::Intercept::operator== const): Added.
+        * inspector/agents/InspectorNetworkAgent.cpp:
+        (WebCore::InspectorNetworkAgent::disable):
+        (WebCore::InspectorNetworkAgent::shouldIntercept): Added.
+        (WebCore::InspectorNetworkAgent::addInterception):
+        (WebCore::InspectorNetworkAgent::removeInterception):
+        (WebCore::InspectorNetworkAgent::willInterceptRequest):
+        (WebCore::InspectorNetworkAgent::shouldInterceptResponse):
+
 2019-11-18  Andres Gonzalez  <andresg_22@apple.com>
 
         Run AccessibilityController::rootElement on secondary thread to simulate HIServices during LayoutTests.
index 9db36da..8eed41d 100644 (file)
@@ -80,6 +80,8 @@
 #include <JavaScriptCore/JSCInlines.h>
 #include <JavaScriptCore/ScriptCallStack.h>
 #include <JavaScriptCore/ScriptCallStackFactory.h>
+#include <wtf/HashMap.h>
+#include <wtf/HashSet.h>
 #include <wtf/JSONValues.h>
 #include <wtf/Lock.h>
 #include <wtf/RefPtr.h>
@@ -87,6 +89,7 @@
 #include <wtf/persistence/PersistentEncoder.h>
 #include <wtf/text/Base64.h>
 #include <wtf/text/StringBuilder.h>
+#include <wtf/text/WTFString.h>
 
 typedef Inspector::NetworkBackendDispatcherHandler::LoadResourceCallback LoadResourceCallback;
 
@@ -830,7 +833,7 @@ void InspectorNetworkAgent::disable(ErrorString&)
 {
     m_enabled = false;
     m_interceptionEnabled = false;
-    m_interceptResponseURLs.clear();
+    m_intercepts.clear();
     m_instrumentingAgents.setInspectorNetworkAgent(nullptr);
     m_resourcesData->clear();
     m_extraRequestHeaders.clear();
@@ -840,6 +843,23 @@ void InspectorNetworkAgent::disable(ErrorString&)
     setResourceCachingDisabled(false);
 }
 
+bool InspectorNetworkAgent::shouldIntercept(URL url)
+{
+    url.removeFragmentIdentifier();
+
+    String urlString = url.string();
+    if (urlString.isEmpty())
+        return false;
+
+    for (auto& intercept : m_intercepts) {
+        auto regex = ContentSearchUtilities::createSearchRegex(intercept.url, intercept.caseSensitive, intercept.isRegex);
+        if (regex.match(urlString) != -1)
+            return true;
+    }
+
+    return false;
+}
+
 void InspectorNetworkAgent::continuePendingResponses()
 {
     for (auto& pendingInterceptResponse : m_pendingInterceptResponses.values())
@@ -1009,7 +1029,7 @@ void InspectorNetworkAgent::setInterceptionEnabled(ErrorString& errorString, boo
         continuePendingResponses();
 }
 
-void InspectorNetworkAgent::addInterception(ErrorString& errorString, const String& url, const String* networkStageString)
+void InspectorNetworkAgent::addInterception(ErrorString& errorString, const String& url, const bool* optionalCaseSensitive, const bool* optionalIsRegex, const String* networkStageString)
 {
     if (networkStageString) {
         auto networkStage = Inspector::Protocol::InspectorHelpers::parseEnumValueFromString<Inspector::Protocol::Network::NetworkStage>(*networkStageString);
@@ -1019,13 +1039,20 @@ void InspectorNetworkAgent::addInterception(ErrorString& errorString, const Stri
         }
     }
 
+    Intercept intercept;
+    intercept.url = url;
+    if (optionalCaseSensitive)
+        intercept.caseSensitive = *optionalCaseSensitive;
+    if (optionalIsRegex)
+        intercept.isRegex = *optionalIsRegex;
+
     // FIXME: Support intercepting requests.
 
-    if (!m_interceptResponseURLs.add(url).isNewEntry)
-        errorString = "Intercept for given url already exists"_s;
+    if (!m_intercepts.appendIfNotContains(intercept))
+        errorString = "Intercept for given url and given isRegex already exists"_s;
 }
 
-void InspectorNetworkAgent::removeInterception(ErrorString& errorString, const String& url, const String* networkStageString)
+void InspectorNetworkAgent::removeInterception(ErrorString& errorString, const String& url, const bool* optionalCaseSensitive, const bool* optionalIsRegex, const String* networkStageString)
 {
     if (networkStageString) {
         auto networkStage = Inspector::Protocol::InspectorHelpers::parseEnumValueFromString<Inspector::Protocol::Network::NetworkStage>(*networkStageString);
@@ -1035,10 +1062,17 @@ void InspectorNetworkAgent::removeInterception(ErrorString& errorString, const S
         }
     }
 
+    Intercept intercept;
+    intercept.url = url;
+    if (optionalCaseSensitive)
+        intercept.caseSensitive = *optionalCaseSensitive;
+    if (optionalIsRegex)
+        intercept.isRegex = *optionalIsRegex;
+
     // FIXME: Support intercepting requests.
 
-    if (!m_interceptResponseURLs.remove(url))
-        errorString = "Missing intercept for given url"_s;
+    if (!m_intercepts.removeAll(intercept))
+        errorString = "Missing intercept for given url and given isRegex"_s;
 }
 
 bool InspectorNetworkAgent::willInterceptRequest(const ResourceRequest& request)
@@ -1046,14 +1080,7 @@ bool InspectorNetworkAgent::willInterceptRequest(const ResourceRequest& request)
     if (!m_interceptionEnabled)
         return false;
 
-    URL requestURL = request.url();
-    requestURL.removeFragmentIdentifier();
-
-    String url = requestURL.string();
-    if (url.isEmpty())
-        return false;
-
-    return m_interceptResponseURLs.contains(url);
+    return shouldIntercept(request.url());
 }
 
 bool InspectorNetworkAgent::shouldInterceptResponse(const ResourceResponse& response)
@@ -1061,14 +1088,7 @@ bool InspectorNetworkAgent::shouldInterceptResponse(const ResourceResponse& resp
     if (!m_interceptionEnabled)
         return false;
 
-    URL responseURL = response.url();
-    responseURL.removeFragmentIdentifier();
-
-    String url = responseURL.string();
-    if (url.isEmpty())
-        return false;
-
-    return m_interceptResponseURLs.contains(url);
+    return shouldIntercept(response.url());
 }
 
 void InspectorNetworkAgent::interceptResponse(const ResourceResponse& response, unsigned long identifier, CompletionHandler<void(const ResourceResponse&, RefPtr<SharedBuffer>)>&& handler)
index 31ef5e3..a68f845 100644 (file)
@@ -37,9 +37,8 @@
 #include <JavaScriptCore/InspectorBackendDispatchers.h>
 #include <JavaScriptCore/InspectorFrontendDispatchers.h>
 #include <JavaScriptCore/RegularExpression.h>
-#include <wtf/HashSet.h>
+#include <wtf/Forward.h>
 #include <wtf/JSONValues.h>
-#include <wtf/text/WTFString.h>
 
 namespace Inspector {
 class InjectedScriptManager;
@@ -89,8 +88,8 @@ public:
     void getSerializedCertificate(ErrorString&, const String& requestId, String* serializedCertificate) final;
     void resolveWebSocket(ErrorString&, const String& requestId, const String* objectGroup, RefPtr<Inspector::Protocol::Runtime::RemoteObject>&) final;
     void setInterceptionEnabled(ErrorString&, bool enabled) final;
-    void addInterception(ErrorString&, const String& url, const String* networkStageString) final;
-    void removeInterception(ErrorString&, const String& url, const String* networkStageString) final;
+    void addInterception(ErrorString&, const String& url, const bool* caseSensitive, const bool* isRegex, const String* networkStageString) final;
+    void removeInterception(ErrorString&, const String& url, const bool* caseSensitive, const bool* isRegex, const String* networkStageString) final;
     void interceptContinue(ErrorString&, const String& requestId) final;
     void interceptWithResponse(ErrorString&, const String& requestId, const String& content, bool base64Encoded, const String* mimeType, const int* status, const String* statusText, const JSON::Object* headers) final;
 
@@ -141,6 +140,7 @@ private:
 
     void willSendRequest(unsigned long identifier, DocumentLoader*, ResourceRequest&, const ResourceResponse& redirectResponse, InspectorPageAgent::ResourceType);
 
+    bool shouldIntercept(URL);
     void continuePendingResponses();
 
     WebSocket* webSocketForRequestId(const String& requestId);
@@ -200,7 +200,19 @@ private:
     HashMap<String, String> m_extraRequestHeaders;
     HashSet<unsigned long> m_hiddenRequestIdentifiers;
 
-    HashSet<String> m_interceptResponseURLs;
+    struct Intercept {
+        String url;
+        bool caseSensitive { true };
+        bool isRegex { false };
+
+        inline bool operator==(const Intercept& other) const
+        {
+            return url == other.url
+                && caseSensitive == other.caseSensitive
+                && isRegex == other.isRegex;
+        }
+    };
+    Vector<Intercept> m_intercepts;
     HashMap<String, std::unique_ptr<PendingInterceptResponse>> m_pendingInterceptResponses;
 
     // FIXME: InspectorNetworkAgent should not be aware of style recalculation.
index 76810fd..e9bc587 100644 (file)
@@ -1,3 +1,54 @@
+2019-11-18  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Local Resource Overrides: allow substitution based on a url pattern
+        https://bugs.webkit.org/show_bug.cgi?id=202375
+
+        Reviewed by Brian Burg.
+
+        Often, websites will load content with a timestamp-based URL query parameter for
+        cache-busting or logging purposes. If a developer is trying to override these resources (or
+        is trying to have an existing override also match these resources), they'd need to edit the
+        local override's URL to match in addition to editing the resource that loads it (e.g. change
+        the <script> in an HTML file), which can sometimes be tricky of the content is dynamically
+        loaded (e.g. an XHR with a non-hardcoded URL).
+
+        Allowing for local overrides to be set using a regular expression pattern would help resolve
+        this limitation.
+
+        * UserInterface/Models/LocalResourceOverride.js:
+        (WI.LocalResourceOverride):
+        (WI.LocalResourceOverride.create):
+        (WI.LocalResourceOverride.fromJSON):
+        (WI.LocalResourceOverride.prototype.toJSON):
+        (WI.LocalResourceOverride.prototype.get isCaseSensitive): Added.
+        (WI.LocalResourceOverride.prototype.get isRegex): Added.
+        (WI.LocalResourceOverride.prototype.matches): Added.
+        (WI.LocalResourceOverride.prototype.saveIdentityToCookie):
+
+        * UserInterface/Controllers/NetworkManager.js:
+        (WI.NetworkManager):
+        (WI.NetworkManager.prototype.initializeTarget):
+        (WI.NetworkManager.prototype.get localResourceOverrides):
+        (WI.NetworkManager.prototype.addLocalResourceOverride):
+        (WI.NetworkManager.prototype.removeLocalResourceOverride):
+        (WI.NetworkManager.prototype.localResourceOverrideForURL):
+        (WI.NetworkManager.prototype.responseIntercepted):
+
+        * UserInterface/Views/LocalResourceOverridePopover.js:
+        (WI.LocalResourceOverridePopover):
+        (WI.LocalResourceOverridePopover.prototype.get serializedData):
+        (WI.LocalResourceOverridePopover.prototype.show):
+        (WI.LocalResourceOverridePopover.prototype._createEditor):
+
+        * UserInterface/Views/LocalResourceOverrideTreeElement.js:
+        (WI.LocalResourceOverrideTreeElement.prototype.get mainTitleText): Added.
+        (WI.LocalResourceOverrideTreeElement.prototype.onattach):
+        (WI.LocalResourceOverrideTreeElement.prototype.ondetach):
+        (WI.LocalResourceOverrideTreeElement.prototype.populateContextMenu):
+        (WI.LocalResourceOverrideTreeElement.prototype.willDismissPopover):
+        * UserInterface/Views/SourcesNavigationSidebarPanel.js:
+        (WI.SourcesNavigationSidebarPanel.prototype._willDismissLocalOverridePopover):
+
 2019-11-15  Devin Rousso  <drousso@apple.com>
 
         Web Inspector: Elements: Styles: support multiline CSS property values
index 3bf3c43..dbda974 100644 (file)
@@ -46,6 +46,7 @@ localizedStrings["%dpx"] = "%dpx";
 localizedStrings["%dpx\u00B2"] = "%dpx\u00B2";
 localizedStrings["%s (%s)"] = "%s (%s)";
 localizedStrings["%s (%s, %s)"] = "%s (%s, %s)";
+localizedStrings["%s (Case Insensitive)"] = "%s (Case Insensitive)";
 localizedStrings["%s (default)"] = "%s (default)";
 localizedStrings["%s (hidden)"] = "%s (hidden)";
 localizedStrings["%s Callback"] = "%s Callback";
index 22656dd..d1018ad 100644 (file)
@@ -43,7 +43,7 @@ WI.NetworkManager = class NetworkManager extends WI.Object
         this._sourceMapURLMap = new Map;
         this._downloadingSourceMaps = new Set;
 
-        this._localResourceOverrideMap = new Map;
+        this._localResourceOverrides = new Set;
         this._harImportLocalResourceMap = new Set;
 
         this._pendingLocalResourceOverrideSaves = null;
@@ -158,9 +158,15 @@ WI.NetworkManager = class NetworkManager extends WI.Object
                 if (this._interceptionEnabled)
                     target.NetworkAgent.setInterceptionEnabled(this._interceptionEnabled);
 
-                for (let [url, localResourceOverride] of this._localResourceOverrideMap) {
-                    if (!localResourceOverride.disabled)
-                        target.NetworkAgent.addInterception(localResourceOverride.url, InspectorBackend.Enum.Network.NetworkStage.Response);
+                for (let localResourceOverride of this._localResourceOverrides) {
+                    if (!localResourceOverride.disabled) {
+                        target.NetworkAgent.addInterception.invoke({
+                            url: localResourceOverride.url,
+                            caseSensitive: localResourceOverride.isCaseSensitive,
+                            isRegex: localResourceOverride.isRegex,
+                            networkStage: InspectorBackend.Enum.Network.NetworkStage.Response,
+                        });
+                    }
                 }
             }
         }
@@ -187,7 +193,7 @@ WI.NetworkManager = class NetworkManager extends WI.Object
 
     get localResourceOverrides()
     {
-        return Array.from(this._localResourceOverrideMap.values());
+        return Array.from(this._localResourceOverrides);
     }
 
     get interceptionEnabled()
@@ -350,17 +356,24 @@ WI.NetworkManager = class NetworkManager extends WI.Object
     {
         console.assert(localResourceOverride instanceof WI.LocalResourceOverride);
 
-        console.assert(!this._localResourceOverrideMap.get(localResourceOverride.url), "Already had an existing local resource override.");
-        this._localResourceOverrideMap.set(localResourceOverride.url, localResourceOverride);
+        console.assert(!this._localResourceOverrides.has(localResourceOverride), "Already had an existing local resource override.");
+        this._localResourceOverrides.add(localResourceOverride);
 
         if (!this._restoringLocalResourceOverrides)
             WI.objectStores.localResourceOverrides.putObject(localResourceOverride);
 
         if (!localResourceOverride.disabled) {
+            let commandArguments = {
+                url: localResourceOverride.url,
+                caseSensitive: localResourceOverride.isCaseSensitive,
+                isRegex: localResourceOverride.isRegex,
+                networkStage: InspectorBackend.Enum.Network.NetworkStage.Response,
+            };
+
             // COMPATIBILITY (iOS 13.0): Network.addInterception did not exist.
             for (let target of WI.targets) {
                 if (target.hasCommand("Network.addInterception"))
-                    target.NetworkAgent.addInterception(localResourceOverride.url, InspectorBackend.Enum.Network.NetworkStage.Response);
+                    target.NetworkAgent.addInterception.invoke(commandArguments);
             }
         }
 
@@ -371,7 +384,7 @@ WI.NetworkManager = class NetworkManager extends WI.Object
     {
         console.assert(localResourceOverride instanceof WI.LocalResourceOverride);
 
-        if (!this._localResourceOverrideMap.delete(localResourceOverride.url)) {
+        if (!this._localResourceOverrides.delete(localResourceOverride)) {
             console.assert(false, "Attempted to remove a local resource override that was not known.");
             return;
         }
@@ -383,10 +396,17 @@ WI.NetworkManager = class NetworkManager extends WI.Object
             WI.objectStores.localResourceOverrides.deleteObject(localResourceOverride);
 
         if (!localResourceOverride.disabled) {
+            let commandArguments = {
+                url: localResourceOverride.url,
+                caseSensitive: localResourceOverride.isCaseSensitive,
+                isRegex: localResourceOverride.isRegex,
+                networkStage: InspectorBackend.Enum.Network.NetworkStage.Response,
+            };
+
             // COMPATIBILITY (iOS 13.0): Network.removeInterception did not exist.
             for (let target of WI.targets) {
                 if (target.hasCommand("Network.removeInterception"))
-                    target.NetworkAgent.removeInterception(localResourceOverride.url, InspectorBackend.Enum.Network.NetworkStage.Response);
+                    target.NetworkAgent.removeInterception.invoke(commandArguments);
             }
         }
 
@@ -395,7 +415,11 @@ WI.NetworkManager = class NetworkManager extends WI.Object
 
     localResourceOverrideForURL(url)
     {
-        return this._localResourceOverrideMap.get(url);
+        for (let localResourceOverride of this._localResourceOverrides) {
+            if (localResourceOverride.matches(url))
+                return localResourceOverride;
+        }
+        return null;
     }
 
     canBeOverridden(resource)
@@ -923,7 +947,7 @@ WI.NetworkManager = class NetworkManager extends WI.Object
     responseIntercepted(target, requestId, response)
     {
         let url = WI.urlWithoutFragment(response.url);
-        let localResourceOverride = this._localResourceOverrideMap.get(url);
+        let localResourceOverride = this.localResourceOverrideForURL(url);
         if (!localResourceOverride || localResourceOverride.disabled) {
             target.NetworkAgent.interceptContinue(requestId);
             return;
@@ -1387,13 +1411,20 @@ WI.NetworkManager = class NetworkManager extends WI.Object
         let localResourceOverride = event.target;
         WI.objectStores.localResourceOverrides.putObject(localResourceOverride);
 
+        let commandArguments = {
+            url: localResourceOverride.url,
+            caseSensitive: localResourceOverride.isCaseSensitive,
+            isRegex: localResourceOverride.isRegex,
+            networkStage: InspectorBackend.Enum.Network.NetworkStage.Response,
+        };
+
         // COMPATIBILITY (iOS 13.0): Network.addInterception / Network.removeInterception did not exist.
         for (let target of WI.targets) {
             if (target.hasDomain("Network")) {
                 if (localResourceOverride.disabled)
-                    target.NetworkAgent.removeInterception(localResourceOverride.url, InspectorBackend.Enum.Network.NetworkStage.Response);
+                    target.NetworkAgent.removeInterception.invoke(commandArguments);
                 else
-                    target.NetworkAgent.addInterception(localResourceOverride.url, InspectorBackend.Enum.Network.NetworkStage.Response);
+                    target.NetworkAgent.addInterception.invoke(commandArguments);
             }
         }
     }
index 51fb453..a038d2a 100644 (file)
 
 WI.LocalResourceOverride = class LocalResourceOverride extends WI.Object
 {
-    constructor(localResource, {disabled} = {})
+    constructor(localResource, {isCaseSensitive, isRegex, disabled} = {})
     {
         console.assert(localResource instanceof WI.LocalResource);
         console.assert(localResource.isLocalResourceOverride);
         console.assert(localResource.url);
-        console.assert(!disabled || typeof disabled === "boolean");
+        console.assert(isCaseSensitive === undefined || typeof isCaseSensitive === "boolean");
+        console.assert(isRegex === undefined || typeof isRegex === "boolean");
+        console.assert(disabled === undefined || typeof disabled === "boolean");
 
         super();
 
         this._localResource = localResource;
-        this._disabled = disabled || false;
+        this._isCaseSensitive = isCaseSensitive !== undefined ? isCaseSensitive : true;
+        this._isRegex = isRegex !== undefined ? isRegex : false;
+        this._disabled = disabled !== undefined ? disabled : false;
     }
 
     // Static
 
-    static create({url, mimeType, content, base64Encoded, statusCode, statusText, headers, disabled})
+    static create({url, mimeType, content, base64Encoded, statusCode, statusText, headers, isCaseSensitive, isRegex, disabled})
     {
         let localResource = new WI.LocalResource({
             request: {
-                url: WI.urlWithoutFragment(url),
+                url: isRegex ? url : WI.urlWithoutFragment(url),
             },
             response: {
                 headers,
@@ -57,21 +61,23 @@ WI.LocalResourceOverride = class LocalResourceOverride extends WI.Object
             isLocalResourceOverride: true,
         });
 
-        return new WI.LocalResourceOverride(localResource, {disabled});
+        return new WI.LocalResourceOverride(localResource, {isCaseSensitive, isRegex, disabled});
     }
 
     // Import / Export
 
     static fromJSON(json)
     {
-        let {localResource, disabled} = json;
-        return new WI.LocalResourceOverride(WI.LocalResource.fromJSON(localResource), {disabled});
+        let {localResource, isCaseSensitive, isRegex, disabled} = json;
+        return new WI.LocalResourceOverride(WI.LocalResource.fromJSON(localResource), {isCaseSensitive, isRegex, disabled});
     }
 
     toJSON(key)
     {
         let json = {
             localResource: this._localResource.toJSON(key),
+            isCaseSensitive: this._isCaseSensitive,
+            isRegex: this._isRegex,
             disabled: this._disabled,
         };
 
@@ -85,6 +91,8 @@ WI.LocalResourceOverride = class LocalResourceOverride extends WI.Object
 
     get url() { return this._localResource.url; }
     get localResource() { return this._localResource; }
+    get isCaseSensitive() { return this._isCaseSensitive; }
+    get isRegex() { return this._isRegex; }
 
     get disabled()
     {
@@ -101,11 +109,26 @@ WI.LocalResourceOverride = class LocalResourceOverride extends WI.Object
         this.dispatchEventToListeners(WI.LocalResourceOverride.Event.DisabledChanged);
     }
 
+    matches(url)
+    {
+        if (this._isRegex) {
+            let regex = new RegExp(this.url, !this._isCaseSensitive ? "i" : "");
+            return regex.test(url);
+        }
+
+        if (!this._isCaseSensitive)
+            return String(url).toLowerCase() === this.url.toLowerCase();
+
+        return url === this.url;
+    }
+
     // Protected
 
     saveIdentityToCookie(cookie)
     {
         cookie["local-resource-override-url"] = this._localResource.url;
+        cookie["local-resource-override-is-case-sensitive"] = this._isCaseSensitive;
+        cookie["local-resource-override-is-regex"] = this._isRegex;
         cookie["local-resource-override-disabled"] = this._disabled;
     }
 };
index feb3001..054bd0f 100644 (file)
     width: 324px;
 }
 
+.popover .local-resource-override-popover-content label:matches(.is-case-sensitive, .is-regex) {
+    display: inline-block;
+}
+
+.popover .local-resource-override-popover-content label.is-case-sensitive {
+    -webkit-margin-end: 8px;
+}
+
 .popover .local-resource-override-popover-content .editor.mime {
     width: 240px;
 }
index c3abb43..e1eba9c 100644 (file)
@@ -30,6 +30,8 @@ WI.LocalResourceOverridePopover = class LocalResourceOverridePopover extends WI.
         super(delegate);
 
         this._urlCodeMirror = null;
+        this._isCaseSensitiveCheckbox = null;
+        this._isRegexCheckbox = null;
         this._mimeTypeCodeMirror = null;
         this._statusCodeCodeMirror = null;
         this._statusTextCodeMirror = null;
@@ -51,9 +53,12 @@ WI.LocalResourceOverridePopover = class LocalResourceOverridePopover extends WI.
         if (!url)
             return null;
 
-        const schemes = ["http:", "https:", "file:"];
-        if (!schemes.some((scheme) => url.startsWith(scheme)))
-            return null;
+        let isRegex = this._isRegexCheckbox && this._isRegexCheckbox.checked;
+        if (!isRegex) {
+            const schemes = ["http:", "https:", "file:"];
+            if (!schemes.some((scheme) => url.toLowerCase().startsWith(scheme)))
+                return null;
+        }
 
         // NOTE: We can allow an empty mimeType / statusCode / statusText to pass
         // network values through, but lets require them for overrides so that
@@ -91,6 +96,12 @@ WI.LocalResourceOverridePopover = class LocalResourceOverridePopover extends WI.
             headers,
         };
 
+        if (this._isCaseSensitiveCheckbox)
+            data.isCaseSensitive = this._isCaseSensitiveCheckbox.checked;
+
+        if (this._isRegexCheckbox)
+            data.isRegex = this._isRegexCheckbox.checked;
+
         // No change.
         let oldSerialized = JSON.stringify(this._serializedDataWhenShown);
         let newSerialized = JSON.stringify(data);
@@ -100,11 +111,13 @@ WI.LocalResourceOverridePopover = class LocalResourceOverridePopover extends WI.
         return data;
     }
 
-    show(localResource, targetElement, preferredEdges)
+    show(localResourceOverride, targetElement, preferredEdges)
     {
         this._targetElement = targetElement;
         this._preferredEdges = preferredEdges;
 
+        let localResource = localResourceOverride ? localResourceOverride.localResource : null;
+
         let url = localResource ? localResource.url : "";
         let mimeType = localResource ? localResource.mimeType : "";
         let statusCode = localResource ? String(localResource.statusCode) : "";
@@ -125,7 +138,7 @@ WI.LocalResourceOverridePopover = class LocalResourceOverridePopover extends WI.
 
         let table = popoverContentElement.appendChild(document.createElement("table"));
 
-        let createRow = (label, id, text) => {
+        let createRow = (label, id, text, placeholder) => {
             let row = table.appendChild(document.createElement("tr"));
             let headerElement = row.appendChild(document.createElement("th"));
             let dataElement = row.appendChild(document.createElement("td"));
@@ -136,7 +149,7 @@ WI.LocalResourceOverridePopover = class LocalResourceOverridePopover extends WI.
             let editorElement = dataElement.appendChild(document.createElement("div"));
             editorElement.classList.add("editor", id);
 
-            let codeMirror = this._createEditor(editorElement, text);
+            let codeMirror = this._createEditor(editorElement, text, placeholder);
             let inputField = codeMirror.getInputField();
             inputField.id = `local-resource-override-popover-${id}-input-field`;
             labelElement.setAttribute("for", inputField.id);
@@ -144,19 +157,56 @@ WI.LocalResourceOverridePopover = class LocalResourceOverridePopover extends WI.
             return {codeMirror, dataElement};
         };
 
-        let urlRow = createRow(WI.UIString("URL"), "url", url);
+        let urlRow = createRow(WI.UIString("URL"), "url", url, url || "http://example.com/index.html");
         this._urlCodeMirror = urlRow.codeMirror;
-        this._urlCodeMirror.setOption("mode", "text/x-local-override-url");
 
-        let mimeTypeRow = createRow(WI.UIString("MIME Type"), "mime", mimeType);
+        let updateURLCodeMirrorMode = () => {
+            let isRegex = this._isRegexCheckbox && this._isRegexCheckbox.checked;
+
+            this._urlCodeMirror.setOption("mode", isRegex ? "text/x-regex" : "text/x-local-override-url");
+
+            if (!isRegex) {
+                let url = this._urlCodeMirror.getValue();
+                const schemes = ["http:", "https:", "file:"];
+                if (!schemes.some((scheme) => url.toLowerCase().startsWith(scheme)))
+                    this._urlCodeMirror.setValue("http://" + url);
+            }
+        };
+
+        if (InspectorBackend.hasCommand("Network.addInterception", "caseSensitive")) {
+            let isCaseSensitiveLabel = urlRow.dataElement.appendChild(document.createElement("label"));
+            isCaseSensitiveLabel.className = "is-case-sensitive";
+
+            this._isCaseSensitiveCheckbox = isCaseSensitiveLabel.appendChild(document.createElement("input"));
+            this._isCaseSensitiveCheckbox.type = "checkbox";
+            this._isCaseSensitiveCheckbox.checked = localResourceOverride ? localResourceOverride.isCaseSensitive : true;
+
+            isCaseSensitiveLabel.append(WI.UIString("Case Sensitive"));
+        }
+
+        if (InspectorBackend.hasCommand("Network.addInterception", "isRegex")) {
+            let isRegexLabel = urlRow.dataElement.appendChild(document.createElement("label"));
+            isRegexLabel.className = "is-regex";
+
+            this._isRegexCheckbox = isRegexLabel.appendChild(document.createElement("input"));
+            this._isRegexCheckbox.type = "checkbox";
+            this._isRegexCheckbox.checked = localResourceOverride ? localResourceOverride.isRegex : false;
+            this._isRegexCheckbox.addEventListener("change", (event) => {
+                updateURLCodeMirrorMode();
+            });
+
+            isRegexLabel.append(WI.UIString("Regular Expression"));
+        }
+
+        let mimeTypeRow = createRow(WI.UIString("MIME Type"), "mime", mimeType, mimeType || "text/html");
         this._mimeTypeCodeMirror = mimeTypeRow.codeMirror;
 
-        let statusCodeRow = createRow(WI.UIString("Status"), "status", statusCode);
+        let statusCodeRow = createRow(WI.UIString("Status"), "status", statusCode, statusCode || "200");
         this._statusCodeCodeMirror = statusCodeRow.codeMirror;
 
         let statusTextEditorElement = statusCodeRow.dataElement.appendChild(document.createElement("div"));
         statusTextEditorElement.className = "editor status-text";
-        this._statusTextCodeMirror = this._createEditor(statusTextEditorElement, statusText);
+        this._statusTextCodeMirror = this._createEditor(statusTextEditorElement, statusText, statusText || "OK");
 
         let editCallback = () => {};
         let deleteCallback = (node) => {
@@ -278,6 +328,9 @@ WI.LocalResourceOverridePopover = class LocalResourceOverridePopover extends WI.
 
         // Update mimeType when URL gets a file extension.
         this._urlCodeMirror.on("change", (cm) => {
+            if (this._isRegexCheckbox && this._isRegexCheckbox.checked)
+                return;
+
             let extension = WI.fileExtensionForURL(cm.getValue());
             if (!extension)
                 return;
@@ -296,6 +349,8 @@ WI.LocalResourceOverridePopover = class LocalResourceOverridePopover extends WI.
             contentTypeDataGridNode.data = {name: "Content-Type", value: mimeType};
         });
 
+        updateURLCodeMirrorMode();
+
         this._serializedDataWhenShown = this.serializedData;
 
         this.content = popoverContentElement;
@@ -315,14 +370,14 @@ WI.LocalResourceOverridePopover = class LocalResourceOverridePopover extends WI.
 
     // Private
 
-    _createEditor(element, value)
+    _createEditor(element, value, placeholder)
     {
         let codeMirror = WI.CodeMirrorEditor.create(element, {
             extraKeys: {"Tab": false, "Shift-Tab": false},
             lineWrapping: false,
             mode: "text/plain",
             matchBrackets: true,
-            placeholder: "http://example.com/index.html",
+            placeholder,
             scrollbarStyle: null,
             value,
         });
index 2f9dbc7..b059631 100644 (file)
@@ -40,6 +40,21 @@ WI.LocalResourceOverrideTreeElement = class LocalResourceOverrideTreeElement ext
 
     // Protected
 
+    get mainTitleText()
+    {
+        let text;
+        if (this.representedObject.isRegex) {
+            text = "/" + this.resource.url + "/";
+            if (!this.representedObject.isCaseSensitive)
+                text += "i";
+        } else {
+            text = super.mainTitleText;
+            if (!this.representedObject.isCaseSensitive)
+                text = WI.UIString("%s (Case Insensitive)").format(text);
+        }
+        return text;
+    }
+
     onattach()
     {
         super.onattach();
@@ -86,7 +101,7 @@ WI.LocalResourceOverrideTreeElement = class LocalResourceOverrideTreeElement ext
     {
         contextMenu.appendItem(WI.UIString("Edit Local Override\u2026"), (event) => {
             let popover = new WI.LocalResourceOverridePopover(this);
-            popover.show(this._localResourceOverride.localResource, this.status, [WI.RectEdge.MAX_X, WI.RectEdge.MIN_X]);
+            popover.show(this._localResourceOverride, this.status, [WI.RectEdge.MAX_X, WI.RectEdge.MIN_X]);
         });
 
         let toggleEnabledString = this._localResourceOverride.disabled ? WI.UIString("Enable Local Override") : WI.UIString("Disable Local Override");
@@ -114,7 +129,7 @@ WI.LocalResourceOverrideTreeElement = class LocalResourceOverrideTreeElement ext
         if (!serializedData)
             return;
 
-        let {url, mimeType, statusCode, statusText, headers} = serializedData;
+        let {url, isCaseSensitive, isRegex, mimeType, statusCode, statusText, headers} = serializedData;
 
         // Do not conflict with an existing override unless we are modifying ourselves.
         let existingOverride = WI.networkManager.localResourceOverrideForURL(url);
@@ -128,6 +143,8 @@ WI.LocalResourceOverrideTreeElement = class LocalResourceOverrideTreeElement ext
         let revision = this._localResourceOverride.localResource.currentRevision;
         let newLocalResourceOverride = WI.LocalResourceOverride.create({
             url,
+            isCaseSensitive,
+            isRegex,
             mimeType,
             statusCode,
             statusText,
index 9d9a30c..a8c15b6 100644 (file)
@@ -667,7 +667,7 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W
             return;
         }
 
-        let {url, mimeType, statusCode, statusText, headers} = serializedData;
+        let {url, isCaseSensitive, isRegex, mimeType, statusCode, statusText, headers} = serializedData;
 
         // Do not conflict with an existing override.
         let existingOverride = WI.networkManager.localResourceOverrideForURL(url);
@@ -678,6 +678,8 @@ WI.SourcesNavigationSidebarPanel = class SourcesNavigationSidebarPanel extends W
 
         let localResourceOverride = WI.LocalResourceOverride.create({
             url,
+            isCaseSensitive,
+            isRegex,
             mimeType,
             statusCode,
             statusText,