Title: [259173] trunk
Revision
259173
Author
[email protected]
Date
2020-03-28 20:07:52 -0700 (Sat, 28 Mar 2020)

Log Message

Web Inspector: support editing cookie key/values from inspector
https://bugs.webkit.org/show_bug.cgi?id=31157
<rdar://problem/19281523>

Reviewed by Timothy Hatcher.

Source/_javascript_Core:

* inspector/protocol/Page.json:
Add a `session` parameter to `Page.Cookie` type and a new `Page.setCookie` command.
Remove the `size` parameter from `Page.Cookie` as this can be calculated in the frontend.

Source/WebCore:

Test: http/tests/inspector/page/setCookie.html

* inspector/agents/InspectorPageAgent.h:
* inspector/agents/InspectorPageAgent.cpp:
(WebCore::buildObjectForCookie):
(WebCore::parseCookieObject): Added.
(WebCore::InspectorPageAgent::setCookie): Added.

* loader/CookieJar.h:
* loader/CookieJar.cpp:
(WebCore::CookieJar::setRawCookie): Added.

Source/WebInspectorUI:

* UserInterface/Models/Cookie.js:
(WI.Cookie):
(WI.Cookie.fromPayload):
(WI.Cookie.parseSetCookieResponseHeader):
(WI.Cookie.prototype.get session): Added.
(WI.Cookie.prototype.expirationDate):
(WI.Cookie.prototype.equals): Added.
(WI.Cookie.prototype.toProtocol): Added.
Add `session` value in addition to the existing `expires` value. Create helper methods for
comparing `WI.Cookie` objects and for using the `WI.Cookie` as a `Page.Cookie` type when
invoking protocol commands (right now just `Page.setCookie`).

* UserInterface/Views/CookieStorageContentView.js:
(WI.CookieStorageContentView):
(WI.CookieStorageContentView.prototype.get navigationItems):
(WI.CookieStorageContentView.prototype.tableCellContextMenuClicked):
(WI.CookieStorageContentView.prototype.willDismissPopover): Added.
(WI.CookieStorageContentView.prototype.async _willDismissCookiePopover): Added.
(WI.CookieStorageContentView.prototype._handleSetCookieButtonClick): Added.
(WI.CookieStorageContentView.prototype._reloadCookies):
(WI.CookieStorageContentView.prototype._formatCookiePropertyForColumn):
Add a + navigation item that shows a popover for creating a new cookie. When contextmenu
clicking on a table row, add an "Edit" item that shows a popover for creating a new cookie
with the values from the existing cookie, which will "replace" (delete and set) the existing
cookie upon being dismissed.

* UserInterface/Views/ResourceCookiesContentView.js:
(WI.ResourceCookiesContentView.prototype.tablePopulateCell):
If only use the `expires` value if `session` is not set.

* UserInterface/Views/CookiePopover.js: Added.
(WI.CookiePopover):
(WI.CookiePopover.prototype.get serializedData):
(WI.CookiePopover.prototype.show.createRow):
(WI.CookiePopover.prototype.show.createInputRow):
(WI.CookiePopover.prototype.show):
(WI.CookiePopover.prototype._presentOverTargetElement):
(WI.CookiePopover.prototype._defaultExpires):
(WI.CookiePopover.prototype._parseExpires):
(WI.CookiePopover.prototype._handleInputKeyDown):
* UserInterface/Views/CookiePopover.css: Added.
(.popover .cookie-popover-content):
(.popover .cookie-popover-content > table):
(.popover .cookie-popover-content > table > tr > th):
(.popover .cookie-popover-content > table > tr > td):
(.popover .cookie-popover-content > table > tr > td > input:matches([type="text"], [type="datetime-local"])):
(.popover .cookie-popover-content > table > tr > td > input:matches([type="text"], [type="datetime-local"]).invalid):
(@media (prefers-color-scheme: dark) .popover .cookie-popover-content > table > tr > th):
Show an  `<input>` (or `<select>`) for each configuration option when creating a cookie.
Hide the `<input>` for `expires` if the `<input type="checkbox">` for `session` is checked.
Indicate when the value in the `<input>` for `expires` is not a valid date.

* UserInterface/Main.html:
* Localizations/en.lproj/localizedStrings.js:

Source/WebKit:

* WebProcess/WebPage/WebCookieJar.h:
* WebProcess/WebPage/WebCookieJar.cpp:
(WebKit::WebCookieJar::setRawCookie):

* NetworkProcess/NetworkConnectionToWebProcess.messages.in:
* NetworkProcess/NetworkConnectionToWebProcess.h:
* NetworkProcess/NetworkConnectionToWebProcess.cpp:
(WebKit::NetworkConnectionToWebProcess::setRawCookie): Added.

LayoutTests:

* http/tests/inspector/page/setCookie.html: Added.
* http/tests/inspector/page/setCookie-expected.txt: Added.

* inspector/unit-tests/number-utilities.html:
* inspector/unit-tests/number-utilities-expected.txt:
Drive-by: add tests for `Number.prototype.maxDecimals`.

Modified Paths

Added Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (259172 => 259173)


--- trunk/LayoutTests/ChangeLog	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/LayoutTests/ChangeLog	2020-03-29 03:07:52 UTC (rev 259173)
@@ -1,5 +1,20 @@
 2020-03-28  Devin Rousso  <[email protected]>
 
+        Web Inspector: support editing cookie key/values from inspector
+        https://bugs.webkit.org/show_bug.cgi?id=31157
+        <rdar://problem/19281523>
+
+        Reviewed by Timothy Hatcher.
+
+        * http/tests/inspector/page/setCookie.html: Added.
+        * http/tests/inspector/page/setCookie-expected.txt: Added.
+
+        * inspector/unit-tests/number-utilities.html:
+        * inspector/unit-tests/number-utilities-expected.txt:
+        Drive-by: add tests for `Number.prototype.maxDecimals`.
+
+2020-03-28  Devin Rousso  <[email protected]>
+
         Web Inspector: CSS: create visual editor for `box-shadow`
         https://bugs.webkit.org/show_bug.cgi?id=208380
 

Added: trunk/LayoutTests/http/tests/inspector/page/setCookie-expected.txt (0 => 259173)


--- trunk/LayoutTests/http/tests/inspector/page/setCookie-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/http/tests/inspector/page/setCookie-expected.txt	2020-03-29 03:07:52 UTC (rev 259173)
@@ -0,0 +1,121 @@
+Test for the Page.setCookie.
+
+
+== Running test suite: Page.setCookie
+-- Running test case: Page.setCookie.Valid
+PASS: Should have been able to set all cookies.
+
+-- Running test case: Page.setCookie.Invalid
+Setting cookie {} ...
+PASS: Should produce an exception.
+Error: Invalid value for key name in given cookie
+
+Setting cookie {"name":null} ...
+PASS: Should produce an exception.
+Error: Invalid value for key name in given cookie
+
+Setting cookie {"name":-1} ...
+PASS: Should produce an exception.
+Error: Invalid value for key name in given cookie
+
+Setting cookie {"name":"name"} ...
+PASS: Should produce an exception.
+Error: Invalid value for key value in given cookie
+
+Setting cookie {"name":"name","value":null} ...
+PASS: Should produce an exception.
+Error: Invalid value for key value in given cookie
+
+Setting cookie {"name":"name","value":-1} ...
+PASS: Should produce an exception.
+Error: Invalid value for key value in given cookie
+
+Setting cookie {"name":"name","value":"value"} ...
+PASS: Should produce an exception.
+Error: Invalid value for key domain in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":null} ...
+PASS: Should produce an exception.
+Error: Invalid value for key domain in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":-1} ...
+PASS: Should produce an exception.
+Error: Invalid value for key domain in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":"webkit.org"} ...
+PASS: Should produce an exception.
+Error: Invalid value for key path in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":"webkit.org","path":null} ...
+PASS: Should produce an exception.
+Error: Invalid value for key path in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":"webkit.org","path":-1} ...
+PASS: Should produce an exception.
+Error: Invalid value for key path in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":"webkit.org","path":"/"} ...
+PASS: Should produce an exception.
+Error: Invalid value for key httpOnly in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":"webkit.org","path":"/","expires":null} ...
+PASS: Should produce an exception.
+Error: Invalid value for key httpOnly in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":"webkit.org","path":"/","expires":"INVALID"} ...
+PASS: Should produce an exception.
+Error: Invalid value for key httpOnly in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":"webkit.org","path":"/","expires":10000000000} ...
+PASS: Should produce an exception.
+Error: Invalid value for key httpOnly in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":"webkit.org","path":"/","expires":10000000000,"session":null} ...
+PASS: Should produce an exception.
+Error: Invalid value for key httpOnly in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":"webkit.org","path":"/","expires":10000000000,"session":"INVALID"} ...
+PASS: Should produce an exception.
+Error: Invalid value for key httpOnly in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":"webkit.org","path":"/","expires":10000000000,"session":true} ...
+PASS: Should produce an exception.
+Error: Invalid value for key httpOnly in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":"webkit.org","path":"/","expires":10000000000,"session":true,"httpOnly":null} ...
+PASS: Should produce an exception.
+Error: Invalid value for key httpOnly in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":"webkit.org","path":"/","expires":10000000000,"session":true,"httpOnly":"INVALID"} ...
+PASS: Should produce an exception.
+Error: Invalid value for key httpOnly in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":"webkit.org","path":"/","expires":10000000000,"session":true,"httpOnly":true} ...
+PASS: Should produce an exception.
+Error: Invalid value for key secure in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":"webkit.org","path":"/","expires":10000000000,"session":true,"httpOnly":true,"secure":null} ...
+PASS: Should produce an exception.
+Error: Invalid value for key secure in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":"webkit.org","path":"/","expires":10000000000,"session":true,"httpOnly":true,"secure":"INVALID"} ...
+PASS: Should produce an exception.
+Error: Invalid value for key secure in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":"webkit.org","path":"/","expires":10000000000,"session":true,"httpOnly":true,"secure":true} ...
+PASS: Should produce an exception.
+Error: Invalid value for key sameSite in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":"webkit.org","path":"/","expires":10000000000,"session":true,"httpOnly":true,"secure":true,"sameSite":null} ...
+PASS: Should produce an exception.
+Error: Invalid value for key sameSite in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":"webkit.org","path":"/","expires":10000000000,"session":true,"httpOnly":true,"secure":true,"sameSite":-1} ...
+PASS: Should produce an exception.
+Error: Invalid value for key sameSite in given cookie
+
+Setting cookie {"name":"name","value":"value","domain":"webkit.org","path":"/","expires":10000000000,"session":true,"httpOnly":true,"secure":true,"sameSite":"INVALID"} ...
+PASS: Should produce an exception.
+Error: Invalid value for key sameSite in given cookie
+
+

Added: trunk/LayoutTests/http/tests/inspector/page/setCookie.html (0 => 259173)


--- trunk/LayoutTests/http/tests/inspector/page/setCookie.html	                        (rev 0)
+++ trunk/LayoutTests/http/tests/inspector/page/setCookie.html	2020-03-29 03:07:52 UTC (rev 259173)
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src=""
+<script>
+if (window.testRunner)
+    testRunner.setAlwaysAcceptCookies(true);
+
+function test()
+{
+    const millisecondsPerDay = 1000 * 60 * 60 * 24;
+    let tomorrow = new Date(Date.now().maxDecimals(-3) + millisecondsPerDay);
+
+    let suite = InspectorTest.createAsyncSuite("Page.setCookie");
+
+    suite.addTestCase({
+        name: "Page.setCookie.Valid",
+        description: "Check that setting a valid cookies works.",
+        async test() {
+            let cookiesToSet = [
+                {
+                    expires: tomorrow,
+                    session: false,
+                    path: "/",
+                    domain: "127.0.0.1",
+                    secure: false,
+                    httpOnly: false,
+                    sameSite: WI.Cookie.SameSiteType.Lax,
+                },
+                {
+                    expires: tomorrow,
+                    session: true,
+                    path: "/inspector/",
+                    domain: "127.0.0.1",
+                    secure: true,
+                    httpOnly: true,
+                    sameSite: WI.Cookie.SameSiteType.Strict,
+                },
+            ].map((options, i) => new WI.Cookie(WI.Cookie.Type.Response, "TestCookieName" + i, "TestCookieValue" + i, options));
+
+            for (let cookieToSet of cookiesToSet)
+                await PageAgent.setCookie(cookieToSet.toProtocol());
+
+            let getCookiesResult = await PageAgent.getCookies();
+            let setCookies = getCookiesResult.cookies.map(WI.Cookie.fromPayload);
+
+            let found = true;
+            for (let cookieToSet of cookiesToSet) {
+                if (!setCookies.find((setCookie) => cookieToSet.equals(setCookie))) {
+                    found = false;
+                    InspectorTest.fail("Missing cookie:");
+                    InspectorTest.json(cookieToSet);
+                }
+            }
+            InspectorTest.expectTrue(found, "Should have been able to set all cookies.");
+        },
+    });
+
+    suite.addTestCase({
+        name: "Page.setCookie.Invalid",
+        description: "Check that the PageAgent correctly parses the given cookie JSON object.",
+        async test() {
+            const invalidValue = null;
+            const invalidString = -1;
+            const invalidNumber = "INVALID";
+            const invalidBoolean = invalidNumber;
+
+            const cookies = [
+                {},
+
+                {name: invalidValue},
+                {name: invalidString},
+                {name: "name"},
+
+                {name: "name", value: invalidValue},
+                {name: "name", value: invalidString},
+                {name: "name", value: "value"},
+
+                {name: "name", value: "value", domain: invalidValue},
+                {name: "name", value: "value", domain: invalidString},
+                {name: "name", value: "value", domain: "webkit.org"},
+
+                {name: "name", value: "value", domain: "webkit.org", path: invalidValue},
+                {name: "name", value: "value", domain: "webkit.org", path: invalidString},
+                {name: "name", value: "value", domain: "webkit.org", path: "/"},
+
+                {name: "name", value: "value", domain: "webkit.org", path: "/", expires: invalidValue},
+                {name: "name", value: "value", domain: "webkit.org", path: "/", expires: invalidNumber},
+                {name: "name", value: "value", domain: "webkit.org", path: "/", expires: 10000000000},
+
+                {name: "name", value: "value", domain: "webkit.org", path: "/", expires: 10000000000, session: invalidValue},
+                {name: "name", value: "value", domain: "webkit.org", path: "/", expires: 10000000000, session: invalidBoolean},
+                {name: "name", value: "value", domain: "webkit.org", path: "/", expires: 10000000000, session: true},
+
+                {name: "name", value: "value", domain: "webkit.org", path: "/", expires: 10000000000, session: true, httpOnly: invalidValue},
+                {name: "name", value: "value", domain: "webkit.org", path: "/", expires: 10000000000, session: true, httpOnly: invalidBoolean},
+                {name: "name", value: "value", domain: "webkit.org", path: "/", expires: 10000000000, session: true, httpOnly: true},
+
+                {name: "name", value: "value", domain: "webkit.org", path: "/", expires: 10000000000, session: true, httpOnly: true, secure: invalidValue},
+                {name: "name", value: "value", domain: "webkit.org", path: "/", expires: 10000000000, session: true, httpOnly: true, secure: invalidBoolean},
+                {name: "name", value: "value", domain: "webkit.org", path: "/", expires: 10000000000, session: true, httpOnly: true, secure: true},
+
+                {name: "name", value: "value", domain: "webkit.org", path: "/", expires: 10000000000, session: true, httpOnly: true, secure: true, sameSite: invalidValue},
+                {name: "name", value: "value", domain: "webkit.org", path: "/", expires: 10000000000, session: true, httpOnly: true, secure: true, sameSite: invalidString},
+                {name: "name", value: "value", domain: "webkit.org", path: "/", expires: 10000000000, session: true, httpOnly: true, secure: true, sameSite: "INVALID"},
+            ];
+            for (let cookie of cookies) {
+                InspectorTest.log("Setting cookie " + JSON.stringify(cookie) + " ...");
+                await InspectorTest.expectException(() => PageAgent.setCookie(cookie));
+                InspectorTest.newline();
+            }
+        },
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body _onload_="runTestHTTPS()">
+    <p>Test for the Page.setCookie.</p>
+</body>
+</html>

Modified: trunk/LayoutTests/inspector/unit-tests/number-utilities-expected.txt (259172 => 259173)


--- trunk/LayoutTests/inspector/unit-tests/number-utilities-expected.txt	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/LayoutTests/inspector/unit-tests/number-utilities-expected.txt	2020-03-29 03:07:52 UTC (rev 259173)
@@ -87,3 +87,14 @@
 PASS: 1000000000 should have 10 digits
 PASS: -1000000000 should have 10 digits
 
+-- Running test case: Number.prototype.maxDecimals
+PASS: maxDecimals with a negative argument more than the number of digits should be 0
+PASS: maxDecimals with a negative argument more equal to the number of digits should be 0
+PASS: maxDecimals with -2 should truncate that 2 units in front of the decimal
+PASS: maxDecimals with -1 should truncate that 1 units in front of the decimal
+PASS: maxDecimals with 0 should round the value
+PASS: maxDecimals with 1 should round after the 1st decimal
+PASS: maxDecimals with 2 should round after the 2nd decimal
+PASS: maxDecimals with a positive argument equal to the number of digits should not modify the number
+PASS: maxDecimals with a positive argument greater than the number of digits should not modify the number
+

Modified: trunk/LayoutTests/inspector/unit-tests/number-utilities.html (259172 => 259173)


--- trunk/LayoutTests/inspector/unit-tests/number-utilities.html	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/LayoutTests/inspector/unit-tests/number-utilities.html	2020-03-29 03:07:52 UTC (rev 259173)
@@ -134,6 +134,23 @@
         }
     });
 
+    suite.addTestCase({
+        name: "Number.prototype.maxDecimals",
+        test() {
+            const n = 123.456;
+
+            InspectorTest.expectEqual(n.maxDecimals(-4), 0, "maxDecimals with a negative argument more than the number of digits should be 0");
+            InspectorTest.expectEqual(n.maxDecimals(-3), 0, "maxDecimals with a negative argument more equal to the number of digits should be 0");
+            InspectorTest.expectEqual(n.maxDecimals(-2), 100, "maxDecimals with -2 should truncate that 2 units in front of the decimal");
+            InspectorTest.expectEqual(n.maxDecimals(-1), 120, "maxDecimals with -1 should truncate that 1 units in front of the decimal");
+            InspectorTest.expectEqual(n.maxDecimals(0), 123, "maxDecimals with 0 should round the value");
+            InspectorTest.expectEqual(n.maxDecimals(1), 123.5, "maxDecimals with 1 should round after the 1st decimal");
+            InspectorTest.expectEqual(n.maxDecimals(2), 123.46, "maxDecimals with 2 should round after the 2nd decimal");
+            InspectorTest.expectEqual(n.maxDecimals(3), 123.456, "maxDecimals with a positive argument equal to the number of digits should not modify the number");
+            InspectorTest.expectEqual(n.maxDecimals(4), 123.456, "maxDecimals with a positive argument greater than the number of digits should not modify the number");
+        },
+    });
+
     suite.runTestCasesAndFinish();
 }
 </script>

Modified: trunk/Source/_javascript_Core/ChangeLog (259172 => 259173)


--- trunk/Source/_javascript_Core/ChangeLog	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/Source/_javascript_Core/ChangeLog	2020-03-29 03:07:52 UTC (rev 259173)
@@ -1,3 +1,15 @@
+2020-03-28  Devin Rousso  <[email protected]>
+
+        Web Inspector: support editing cookie key/values from inspector
+        https://bugs.webkit.org/show_bug.cgi?id=31157
+        <rdar://problem/19281523>
+
+        Reviewed by Timothy Hatcher.
+
+        * inspector/protocol/Page.json:
+        Add a `session` parameter to `Page.Cookie` type and a new `Page.setCookie` command.
+        Remove the `size` parameter from `Page.Cookie` as this can be calculated in the frontend.
+
 2020-03-27  Ross Kirsling  <[email protected]>
 
         [JSC] Make Operator an enum class to avoid Op* identifiers

Modified: trunk/Source/_javascript_Core/inspector/protocol/Page.json (259172 => 259173)


--- trunk/Source/_javascript_Core/inspector/protocol/Page.json	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/Source/_javascript_Core/inspector/protocol/Page.json	2020-03-29 03:07:52 UTC (rev 259173)
@@ -104,10 +104,9 @@
                 { "name": "domain", "type": "string", "description": "Cookie domain." },
                 { "name": "path", "type": "string", "description": "Cookie path." },
                 { "name": "expires", "type": "number", "description": "Cookie expires." },
-                { "name": "size", "type": "integer", "description": "Cookie size." },
+                { "name": "session", "type": "boolean", "description": "True in case of session cookie." },
                 { "name": "httpOnly", "type": "boolean", "description": "True if cookie is http-only." },
                 { "name": "secure", "type": "boolean", "description": "True if cookie is secure." },
-                { "name": "session", "type": "boolean", "description": "True in case of session cookie." },
                 { "name": "sameSite", "$ref": "CookieSameSitePolicy", "description": "Cookie Same-Site policy." }
             ]
         }
@@ -159,8 +158,15 @@
             ]
         },
         {
+            "name": "setCookie",
+            "description": "Sets a new browser cookie with the given name, domain, and path.",
+            "parameters": [
+                { "name": "cookie", "$ref": "Cookie" }
+            ]
+        },
+        {
             "name": "deleteCookie",
-            "description": "Deletes browser cookie with given name, domain and path.",
+            "description": "Deletes browser cookie with given name, domain, and path.",
             "parameters": [
                 { "name": "cookieName", "type": "string", "description": "Name of the cookie to remove." },
                 { "name": "url", "type": "string", "description": "URL to match cookie domain and path." }

Modified: trunk/Source/WebCore/ChangeLog (259172 => 259173)


--- trunk/Source/WebCore/ChangeLog	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/Source/WebCore/ChangeLog	2020-03-29 03:07:52 UTC (rev 259173)
@@ -1,3 +1,23 @@
+2020-03-28  Devin Rousso  <[email protected]>
+
+        Web Inspector: support editing cookie key/values from inspector
+        https://bugs.webkit.org/show_bug.cgi?id=31157
+        <rdar://problem/19281523>
+
+        Reviewed by Timothy Hatcher.
+
+        Test: http/tests/inspector/page/setCookie.html
+
+        * inspector/agents/InspectorPageAgent.h:
+        * inspector/agents/InspectorPageAgent.cpp:
+        (WebCore::buildObjectForCookie):
+        (WebCore::parseCookieObject): Added.
+        (WebCore::InspectorPageAgent::setCookie): Added.
+
+        * loader/CookieJar.h:
+        * loader/CookieJar.cpp:
+        (WebCore::CookieJar::setRawCookie): Added.
+
 2020-03-28  Simon Fraser  <[email protected]>
 
         Add a ScrollLatching log channel and improve some logging functionality

Modified: trunk/Source/WebCore/inspector/agents/InspectorPageAgent.cpp (259172 => 259173)


--- trunk/Source/WebCore/inspector/agents/InspectorPageAgent.cpp	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/Source/WebCore/inspector/agents/InspectorPageAgent.cpp	2020-03-29 03:07:52 UTC (rev 259173)
@@ -489,10 +489,9 @@
         .setDomain(cookie.domain)
         .setPath(cookie.path)
         .setExpires(cookie.expires.valueOr(0))
-        .setSize((cookie.name.length() + cookie.value.length()))
+        .setSession(cookie.session)
         .setHttpOnly(cookie.httpOnly)
         .setSecure(cookie.secure)
-        .setSession(cookie.session)
         .setSameSite(cookieSameSitePolicyJSON(cookie.sameSite))
         .release();
 }
@@ -560,6 +559,90 @@
         cookies = JSON::ArrayOf<Inspector::Protocol::Page::Cookie>::create();
 }
 
+static Optional<Cookie> parseCookieObject(ErrorString& errorString, const JSON::Object& cookieObject)
+{
+    Cookie cookie;
+
+    if (!cookieObject.getString("name"_s, cookie.name)) {
+        errorString = "Invalid value for key name in given cookie";
+        return WTF::nullopt;
+    }
+
+    if (!cookieObject.getString("value"_s, cookie.value)) {
+        errorString = "Invalid value for key value in given cookie";
+        return WTF::nullopt;
+    }
+
+    if (!cookieObject.getString("domain"_s, cookie.domain)) {
+        errorString = "Invalid value for key domain in given cookie";
+        return WTF::nullopt;
+    }
+
+    if (!cookieObject.getString("path"_s, cookie.path)) {
+        errorString = "Invalid value for key path in given cookie";
+        return WTF::nullopt;
+    }
+
+    if (!cookieObject.getBoolean("httpOnly"_s, cookie.httpOnly)) {
+        errorString = "Invalid value for key httpOnly in given cookie";
+        return WTF::nullopt;
+    }
+
+    if (!cookieObject.getBoolean("secure"_s, cookie.secure)) {
+        errorString = "Invalid value for key secure in given cookie";
+        return WTF::nullopt;
+    }
+
+    bool validSession = cookieObject.getBoolean("session"_s, cookie.session);
+    cookie.expires = cookieObject.getNumber<double>("expires"_s);
+    if (!validSession && !cookie.expires) {
+        errorString = "Invalid value for key expires in given cookie";
+        return WTF::nullopt;
+    }
+
+    String sameSiteString;
+    if (!cookieObject.getString("sameSite"_s, sameSiteString)) {
+        errorString = "Invalid value for key sameSite in given cookie";
+        return WTF::nullopt;
+    }
+
+    auto sameSite = Inspector::Protocol::InspectorHelpers::parseEnumValueFromString<Inspector::Protocol::Page::CookieSameSitePolicy>(sameSiteString);
+    if (!sameSite) {
+        errorString = "Invalid value for key sameSite in given cookie";
+        return WTF::nullopt;
+    }
+
+    switch (sameSite.value()) {
+    case Inspector::Protocol::Page::CookieSameSitePolicy::None:
+        cookie.sameSite = Cookie::SameSitePolicy::None;
+
+        break;
+    case Inspector::Protocol::Page::CookieSameSitePolicy::Lax:
+        cookie.sameSite = Cookie::SameSitePolicy::Lax;
+
+        break;
+    case Inspector::Protocol::Page::CookieSameSitePolicy::Strict:
+        cookie.sameSite = Cookie::SameSitePolicy::Strict;
+        break;
+    }
+
+    return cookie;
+}
+
+void InspectorPageAgent::setCookie(ErrorString& errorString, const JSON::Object& cookieObject)
+{
+    auto cookie = parseCookieObject(errorString, cookieObject);
+    if (!cookie)
+        return;
+
+    for (auto* frame = &m_inspectedPage.mainFrame(); frame; frame = frame->tree().traverseNext()) {
+        if (auto* document = frame->document()) {
+            if (auto* page = document->page())
+                page->cookieJar().setRawCookie(*document, cookie.value());
+        }
+    }
+}
+
 void InspectorPageAgent::deleteCookie(ErrorString&, const String& cookieName, const String& url)
 {
     URL parsedURL({ }, url);

Modified: trunk/Source/WebCore/inspector/agents/InspectorPageAgent.h (259172 => 259173)


--- trunk/Source/WebCore/inspector/agents/InspectorPageAgent.h	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/Source/WebCore/inspector/agents/InspectorPageAgent.h	2020-03-29 03:07:52 UTC (rev 259173)
@@ -101,6 +101,7 @@
     void overrideUserAgent(ErrorString&, const String* value) override;
     void overrideSetting(ErrorString&, const String& setting, const bool* value) override;
     void getCookies(ErrorString&, RefPtr<JSON::ArrayOf<Inspector::Protocol::Page::Cookie>>& cookies) override;
+    void setCookie(ErrorString&, const JSON::Object& cookieObject) override;
     void deleteCookie(ErrorString&, const String& cookieName, const String& url) override;
     void getResourceTree(ErrorString&, RefPtr<Inspector::Protocol::Page::FrameResourceTree>&) override;
     void getResourceContent(ErrorString&, const String& frameId, const String& url, String* content, bool* base64Encoded) override;

Modified: trunk/Source/WebCore/loader/CookieJar.cpp (259172 => 259173)


--- trunk/Source/WebCore/loader/CookieJar.cpp	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/Source/WebCore/loader/CookieJar.cpp	2020-03-29 03:07:52 UTC (rev 259173)
@@ -171,6 +171,14 @@
     return false;
 }
 
+void CookieJar::setRawCookie(const Document&, const Cookie& cookie)
+{
+    if (auto* session = m_storageSessionProvider->storageSession())
+        session->setCookie(cookie);
+    else
+        ASSERT_NOT_REACHED();
+}
+
 void CookieJar::deleteCookie(const Document&, const URL& url, const String& cookieName)
 {
     if (auto* session = m_storageSessionProvider->storageSession())

Modified: trunk/Source/WebCore/loader/CookieJar.h (259172 => 259173)


--- trunk/Source/WebCore/loader/CookieJar.h	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/Source/WebCore/loader/CookieJar.h	2020-03-29 03:07:52 UTC (rev 259173)
@@ -58,6 +58,7 @@
     virtual bool cookiesEnabled(const Document&) const;
     virtual std::pair<String, SecureCookiesAccessed> cookieRequestHeaderFieldValue(const URL& firstParty, const SameSiteInfo&, const URL&, Optional<FrameIdentifier>, Optional<PageIdentifier>, IncludeSecureCookies) const;
     virtual bool getRawCookies(const Document&, const URL&, Vector<Cookie>&) const;
+    virtual void setRawCookie(const Document&, const Cookie&);
     virtual void deleteCookie(const Document&, const URL&, const String& cookieName);
 
     // Cookie Cache.

Modified: trunk/Source/WebInspectorUI/ChangeLog (259172 => 259173)


--- trunk/Source/WebInspectorUI/ChangeLog	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/Source/WebInspectorUI/ChangeLog	2020-03-29 03:07:52 UTC (rev 259173)
@@ -1,5 +1,68 @@
 2020-03-28  Devin Rousso  <[email protected]>
 
+        Web Inspector: support editing cookie key/values from inspector
+        https://bugs.webkit.org/show_bug.cgi?id=31157
+        <rdar://problem/19281523>
+
+        Reviewed by Timothy Hatcher.
+
+        * UserInterface/Models/Cookie.js:
+        (WI.Cookie):
+        (WI.Cookie.fromPayload):
+        (WI.Cookie.parseSetCookieResponseHeader):
+        (WI.Cookie.prototype.get session): Added.
+        (WI.Cookie.prototype.expirationDate):
+        (WI.Cookie.prototype.equals): Added.
+        (WI.Cookie.prototype.toProtocol): Added.
+        Add `session` value in addition to the existing `expires` value. Create helper methods for
+        comparing `WI.Cookie` objects and for using the `WI.Cookie` as a `Page.Cookie` type when
+        invoking protocol commands (right now just `Page.setCookie`).
+
+        * UserInterface/Views/CookieStorageContentView.js:
+        (WI.CookieStorageContentView):
+        (WI.CookieStorageContentView.prototype.get navigationItems):
+        (WI.CookieStorageContentView.prototype.tableCellContextMenuClicked):
+        (WI.CookieStorageContentView.prototype.willDismissPopover): Added.
+        (WI.CookieStorageContentView.prototype.async _willDismissCookiePopover): Added.
+        (WI.CookieStorageContentView.prototype._handleSetCookieButtonClick): Added.
+        (WI.CookieStorageContentView.prototype._reloadCookies):
+        (WI.CookieStorageContentView.prototype._formatCookiePropertyForColumn):
+        Add a + navigation item that shows a popover for creating a new cookie. When contextmenu
+        clicking on a table row, add an "Edit" item that shows a popover for creating a new cookie
+        with the values from the existing cookie, which will "replace" (delete and set) the existing
+        cookie upon being dismissed.
+
+        * UserInterface/Views/ResourceCookiesContentView.js:
+        (WI.ResourceCookiesContentView.prototype.tablePopulateCell):
+        If only use the `expires` value if `session` is not set.
+
+        * UserInterface/Views/CookiePopover.js: Added.
+        (WI.CookiePopover):
+        (WI.CookiePopover.prototype.get serializedData):
+        (WI.CookiePopover.prototype.show.createRow):
+        (WI.CookiePopover.prototype.show.createInputRow):
+        (WI.CookiePopover.prototype.show):
+        (WI.CookiePopover.prototype._presentOverTargetElement):
+        (WI.CookiePopover.prototype._defaultExpires):
+        (WI.CookiePopover.prototype._parseExpires):
+        (WI.CookiePopover.prototype._handleInputKeyDown):
+        * UserInterface/Views/CookiePopover.css: Added.
+        (.popover .cookie-popover-content):
+        (.popover .cookie-popover-content > table):
+        (.popover .cookie-popover-content > table > tr > th):
+        (.popover .cookie-popover-content > table > tr > td):
+        (.popover .cookie-popover-content > table > tr > td > input:matches([type="text"], [type="datetime-local"])):
+        (.popover .cookie-popover-content > table > tr > td > input:matches([type="text"], [type="datetime-local"]).invalid):
+        (@media (prefers-color-scheme: dark) .popover .cookie-popover-content > table > tr > th):
+        Show an  `<input>` (or `<select>`) for each configuration option when creating a cookie.
+        Hide the `<input>` for `expires` if the `<input type="checkbox">` for `session` is checked.
+        Indicate when the value in the `<input>` for `expires` is not a valid date.
+
+        * UserInterface/Main.html:
+        * Localizations/en.lproj/localizedStrings.js:
+
+2020-03-28  Devin Rousso  <[email protected]>
+
         Web Inspector: CSS: create visual editor for `box-shadow`
         https://bugs.webkit.org/show_bug.cgi?id=208380
 

Modified: trunk/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js (259172 => 259173)


--- trunk/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js	2020-03-29 03:07:52 UTC (rev 259173)
@@ -99,6 +99,7 @@
 localizedStrings["Add Action"] = "Add Action";
 localizedStrings["Add Breakpoint"] = "Add Breakpoint";
 localizedStrings["Add Breakpoints"] = "Add Breakpoints";
+localizedStrings["Add Cookie"] = "Add Cookie";
 localizedStrings["Add Header"] = "Add Header";
 localizedStrings["Add New"] = "Add New";
 localizedStrings["Add New Class"] = "Add New Class";

Modified: trunk/Source/WebInspectorUI/UserInterface/Main.html (259172 => 259173)


--- trunk/Source/WebInspectorUI/UserInterface/Main.html	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/Source/WebInspectorUI/UserInterface/Main.html	2020-03-29 03:07:52 UTC (rev 259173)
@@ -80,6 +80,7 @@
     <link rel="stylesheet" href=""
     <link rel="stylesheet" href=""
     <link rel="stylesheet" href=""
+    <link rel="stylesheet" href=""
     <link rel="stylesheet" href=""
     <link rel="stylesheet" href=""
     <link rel="stylesheet" href=""
@@ -645,6 +646,7 @@
     <script src=""
     <script src=""
     <script src=""
+    <script src=""
     <script src=""
     <script src=""
     <script src=""

Modified: trunk/Source/WebInspectorUI/UserInterface/Models/Cookie.js (259172 => 259173)


--- trunk/Source/WebInspectorUI/UserInterface/Models/Cookie.js	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/Source/WebInspectorUI/UserInterface/Models/Cookie.js	2020-03-29 03:07:52 UTC (rev 259173)
@@ -25,7 +25,7 @@
 
 WI.Cookie = class Cookie
 {
-    constructor(type, name, value, {header, expires, maxAge, path, domain, secure, httpOnly, sameSite, size} = {})
+    constructor(type, name, value, {header, expires, session, maxAge, path, domain, secure, httpOnly, sameSite} = {})
     {
         console.assert(Object.values(WI.Cookie.Type).includes(type));
         console.assert(typeof name === "string");
@@ -32,6 +32,7 @@
         console.assert(typeof value === "string");
         console.assert(!header || typeof header === "string");
         console.assert(!expires || expires instanceof Date);
+        console.assert(!session || typeof session === "boolean");
         console.assert(!maxAge || typeof maxAge === "number");
         console.assert(!path || typeof path === "string");
         console.assert(!domain || typeof domain === "string");
@@ -38,16 +39,16 @@
         console.assert(!secure || typeof secure === "boolean");
         console.assert(!httpOnly || typeof httpOnly === "boolean");
         console.assert(!sameSite || Object.values(WI.Cookie.SameSiteType).includes(sameSite));
-        console.assert(!size || typeof size === "number");
 
         this._type = type;
         this._name = name;
         this._value = value;
-        this._size = size || this._name.length + this._value.length;
+        this._size = this._name.length + this._value.length;
 
         if (this._type === WI.Cookie.Type.Response) {
             this._header = header || "";
-            this._expires = expires || null;
+            this._expires = (!session && expires) || null;
+            this._session = session || false;
             this._maxAge = maxAge || null;
             this._path = path || null;
             this._domain = domain || null;
@@ -62,7 +63,7 @@
     static fromPayload(payload)
     {
         let {name, value, ...options} = payload;
-        options.expires = options.expires ? new Date(options.expires) : null;
+        options.expires = options.expires ? new Date(options.expires.maxDecimals(-3)) : null;
 
         return new WI.Cookie(WI.Cookie.Type.Response, name, value, options);
     }
@@ -148,6 +149,7 @@
 
         let {name, value} = nameValueMatch.groups;
         let expires = null;
+        let session = false;
         let maxAge = null;
         let path = null;
         let domain = null;
@@ -212,7 +214,10 @@
             }
         }
 
-        return new WI.Cookie(WI.Cookie.Type.Response, name, value, {header, expires, maxAge, path, domain, secure, httpOnly, sameSite});
+        if (!expires)
+            session = true;
+
+        return new WI.Cookie(WI.Cookie.Type.Response, name, value, {header, expires, session, maxAge, path, domain, secure, httpOnly, sameSite});
     }
 
     // Public
@@ -222,6 +227,7 @@
     get value() { return this._value; }
     get header() { return this._header; }
     get expires() { return this._expires; }
+    get session() { return this._session; }
     get maxAge() { return this._maxAge; }
     get path() { return this._path; }
     get domain() { return this._domain; }
@@ -240,6 +246,9 @@
 
     expirationDate(requestSentDate)
     {
+        if (this._session)
+            return null;
+
         if (this._maxAge) {
             let startDate = requestSentDate || new Date;
             return new Date(startDate.getTime() + (this._maxAge * 1000));
@@ -247,6 +256,57 @@
 
         return this._expires;
     }
+
+    equals(other)
+    {
+        return this._type === other.type
+            && this._name === other.name
+            && this._value === other.value
+            && this._header === other.header
+            && this._expires?.getTime() === other.expires?.getTime()
+            && this._session === other.session
+            && this._maxAge === other.maxAge
+            && this._path === other.path
+            && this._domain === other.domain
+            && this._secure === other.secure
+            && this._httpOnly === other.httpOnly
+            && this._sameSite === other.sameSite;
+    }
+
+    toProtocol()
+    {
+        if (typeof this._name !== "string")
+            return null;
+
+        if (typeof this._value !== "string")
+            return null;
+
+        if (typeof this._domain !== "string")
+            return null;
+
+        if (typeof this._path !== "string")
+            return null;
+
+        if (!this._session && !this._expires)
+            return null;
+
+        if (!Object.values(WI.Cookie.SameSiteType).includes(this._sameSite))
+            return null;
+
+        let json = {
+            name: this._name,
+            value: this._value,
+            domain: this._domain,
+            path: this._path,
+            expires: this._expires?.getTime(),
+            session: this._session,
+            httpOnly: !!this._httpOnly,
+            secure: !!this._secure,
+            sameSite: this._sameSite,
+        };
+
+        return json;
+    }
 };
 
 WI.Cookie.Type = {

Added: trunk/Source/WebInspectorUI/UserInterface/Views/CookiePopover.css (0 => 259173)


--- trunk/Source/WebInspectorUI/UserInterface/Views/CookiePopover.css	                        (rev 0)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/CookiePopover.css	2020-03-29 03:07:52 UTC (rev 259173)
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2020 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.popover .cookie-popover-content {
+    max-width: 420px;
+    padding: 4px 8px;
+}
+
+.popover .cookie-popover-content > table {
+    width: 100%;
+}
+
+.popover .cookie-popover-content > table > tr > th {
+    font-weight: bold;
+    text-align: end;
+    line-height: 23px;
+    vertical-align: top;
+    white-space: nowrap;
+    color: hsl(0, 0%, 34%);
+}
+
+.popover .cookie-popover-content > table > tr > td {
+    -webkit-padding-start: 4px;
+}
+
+/* FIXME: <https://webkit.org/b/209389> Web Inspector: use native datetime-local picker for changing `expires` value in cookie popover */
+
+.popover .cookie-popover-content > table > tr > td > input:matches([type="text"], [type="datetime-local"]) {
+    width: 100%;
+    padding: 3px 4px 2px;
+    font-family: Menlo, monospace;
+    background-color: var(--background-color-code);
+    border: 1px solid var(--text-color-quaternary);
+    -webkit-appearance: none;
+}
+
+.popover .cookie-popover-content > table > tr > td > input:matches([type="text"], [type="datetime-local"]).invalid {
+    color: var(--error-text-color);
+}
+
+@media (prefers-color-scheme: dark) {
+    .popover .cookie-popover-content > table > tr > th {
+        color: var(--text-color-secondary);
+    }
+}

Added: trunk/Source/WebInspectorUI/UserInterface/Views/CookiePopover.js (0 => 259173)


--- trunk/Source/WebInspectorUI/UserInterface/Views/CookiePopover.js	                        (rev 0)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/CookiePopover.js	2020-03-29 03:07:52 UTC (rev 259173)
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2020 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.CookiePopover = class CookiePopover extends WI.Popover
+{
+    constructor(delegate)
+    {
+        super(delegate);
+
+        this._nameInputElement = null;
+        this._valueInputElement = null;
+        this._domainInputElement = null;
+        this._pathInputElement = null;
+        this._sessionCheckboxElement = null;
+        this._expiresInputElement = null;
+        this._httpOnlyCheckboxElement = null;
+        this._secureCheckboxElement = null;
+        this._sameSiteSelectElement = null;
+
+        this._serializedDataWhenShown = null;
+
+        this.windowResizeHandler = this._presentOverTargetElement.bind(this);
+    }
+
+    // Public
+
+    get serializedData()
+    {
+        if (!this._targetElement)
+            return null;
+
+        let name = this._nameInputElement.value || this._nameInputElement.placeholder;
+        if (!name)
+            return null;
+
+        let value = this._valueInputElement.value || this._valueInputElement.placeholder;
+        if (!value)
+            return null;
+
+        let domain = this._domainInputElement.value || this._domainInputElement.placeholder;
+        if (!domain)
+            return null;
+
+        let path = this._pathInputElement.value || this._pathInputElement.placeholder;
+        if (!path)
+            return null;
+
+        let session = this._sessionCheckboxElement.checked;
+        let expires = this._parseExpires();
+        if (!session && isNaN(expires))
+            return null;
+
+        // If a full URL is entered in the domain input, parse it to get just the domain.
+        try {
+            let url = "" URL(domain);
+            domain = url.hostname;
+        } catch { }
+
+        if (!path.startsWith("/"))
+            path = "/" + path;
+
+        let data = {
+            name,
+            value,
+            domain,
+            path,
+            httpOnly: this._httpOnlyCheckboxElement.checked,
+            secure: this._secureCheckboxElement.checked,
+            sameSite: this._sameSiteSelectElement.value,
+        };
+
+        if (session)
+            data.session = true;
+        else
+            data.expires = expires;
+
+        if (JSON.stringify(data) === JSON.stringify(this._serializedDataWhenShown))
+            return null;
+
+        return data;
+    }
+
+    show(cookie, targetElement, preferredEdges)
+    {
+        console.assert(!cookie || cookie instanceof WI.Cookie, cookie);
+        console.assert(targetElement instanceof Element, targetElement);
+        console.assert(Array.isArray(preferredEdges), preferredEdges);
+
+        this._targetElement = targetElement;
+        this._preferredEdges = preferredEdges;
+
+        let data = ""
+        if (cookie) {
+            data.name = cookie.name;
+            data.value = cookie.value;
+            data.domain = cookie.domain;
+            data.path = cookie.path;
+            data.expires = (cookie.expires || this._defaultExpires()).toLocaleString();
+            data.session = cookie.session;
+            data.httpOnly = cookie.httpOnly;
+            data.secure = cookie.secure;
+            data.sameSite = cookie.sameSite;
+        } else {
+            let urlComponents = WI.networkManager.mainFrame.mainResource.urlComponents;
+            data.name = WI.unlocalizedString("name");
+            data.value = WI.unlocalizedString("value");
+            data.domain = urlComponents.host;
+            data.path = urlComponents.path;
+            data.expires = this._defaultExpires().toLocaleString();
+            data.session = true;
+            data.httpOnly = false;
+            data.secure = false;
+            data.sameSite = WI.Cookie.SameSiteType.None;
+        }
+
+        let popoverContentElement = document.createElement("div");
+        popoverContentElement.className = "cookie-popover-content";
+
+        let tableElement = popoverContentElement.appendChild(document.createElement("table"));
+
+        function createRow(id, label, editorElement) {
+            id = `cookie-popover-${id}-editor`;
+
+            let rowElement = tableElement.appendChild(document.createElement("tr"));
+
+            let headerElement = rowElement.appendChild(document.createElement("th"));
+
+            let labelElement = headerElement.appendChild(document.createElement("label"));
+            labelElement.setAttribute("for", id);
+            labelElement.textContent = label;
+
+            let dataElement = rowElement.appendChild(document.createElement("td"));
+
+            editorElement.id = id;
+            dataElement.appendChild(editorElement);
+
+            return {rowElement};
+        }
+
+        let boundHandleInputKeyDown = this._handleInputKeyDown.bind(this);
+
+        function createInputRow(id, label, type, value) {
+            let inputElement = document.createElement("input");
+            inputElement.type = type;
+
+            if (type === "checkbox")
+                inputElement.checked = value;
+            else {
+                if (cookie)
+                    inputElement.value = value;
+                inputElement.placeholder = value;
+                inputElement.addEventListener("keydown", boundHandleInputKeyDown);
+            }
+
+            let rowElement = createRow(id, label, inputElement).rowElement;
+
+            return {inputElement, rowElement};
+        }
+
+        this._nameInputElement = createInputRow("name", WI.UIString("Name"), "text", data.name).inputElement;
+
+        this._valueInputElement = createInputRow("value", WI.UIString("Value"), "text", data.value).inputElement;
+
+        this._domainInputElement = createInputRow("domain", WI.unlocalizedString("Domain"), "text", data.domain).inputElement;
+
+        this._pathInputElement = createInputRow("path", WI.unlocalizedString("Path"), "text", data.path).inputElement;
+
+        this._sessionCheckboxElement = createInputRow("session", WI.unlocalizedString("Session"), "checkbox", data.session).inputElement;
+
+        let expiresInputRow = createInputRow("expires", WI.unlocalizedString("Expires"), "datetime-local", data.expires);
+        this._expiresInputElement = expiresInputRow.inputElement;
+        this._expiresInputElement.addEventListener("input", (event) => {
+            this._expiresInputElement.classList.toggle("invalid", isNaN(this._parseExpires()));
+        });
+
+        this._httpOnlyCheckboxElement = createInputRow("http-only", WI.unlocalizedString("HttpOnly"), "checkbox", data.httpOnly).inputElement;
+
+        this._secureCheckboxElement = createInputRow("secure", WI.unlocalizedString("Secure"), "checkbox", data.secure).inputElement;
+
+        this._sameSiteSelectElement = document.createElement("select");
+        for (let sameSiteType of Object.values(WI.Cookie.SameSiteType)) {
+            let optionElement = this._sameSiteSelectElement.appendChild(document.createElement("option"));
+            optionElement.textContent = sameSiteType;
+        }
+        createRow("same-site", WI.unlocalizedString("SameSite"), this._sameSiteSelectElement);
+
+        let toggleExpiresRow = () => {
+            expiresInputRow.rowElement.hidden = this._sessionCheckboxElement.checked;
+
+            this.update();
+        };
+
+        this._sessionCheckboxElement.addEventListener("change", (event) => {
+            toggleExpiresRow();
+        });
+
+        toggleExpiresRow();
+
+        this._serializedDataWhenShown = this.serializedData;
+
+        this.content = popoverContentElement;
+        this._presentOverTargetElement();
+    }
+
+    // Private
+
+    _presentOverTargetElement()
+    {
+        if (!this._targetElement)
+            return;
+
+        let targetFrame = WI.Rect.rectFromClientRect(this._targetElement.getBoundingClientRect());
+        this.present(targetFrame.pad(2), this._preferredEdges);
+    }
+
+    _defaultExpires()
+    {
+        return new Date(Date.now() + (1000 * 60 * 60 * 24)); // one day in the future
+    }
+
+    _parseExpires()
+    {
+        let timestamp = Date.parse(this._expiresInputElement.value || this._expiresInputElement.placeholder);
+        if (timestamp < Date.now())
+            return NaN;
+        return timestamp;
+    }
+
+    _handleInputKeyDown(event)
+    {
+        if (event.key === "Enter" || event.key === "Esc")
+            this.dismiss();
+    }
+};

Modified: trunk/Source/WebInspectorUI/UserInterface/Views/CookieStorageContentView.js (259172 => 259173)


--- trunk/Source/WebInspectorUI/UserInterface/Views/CookieStorageContentView.js	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/CookieStorageContentView.js	2020-03-29 03:07:52 UTC (rev 259173)
@@ -35,6 +35,11 @@
         this._sortComparator = null;
         this._table = null;
 
+        if (InspectorBackend.hasCommand("Page.setCookie")) {
+            this._setCookieButtonNavigationItem = new WI.ButtonNavigationItem("cookie-storage-set-cookie", WI.UIString("Add Cookie"), "Images/Plus15.svg", 15, 15);
+            this._setCookieButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleSetCookieButtonClick, this);
+        }
+
         this._refreshButtonNavigationItem = new WI.ButtonNavigationItem("cookie-storage-refresh", WI.UIString("Refresh"), "Images/ReloadFull.svg", 13, 13);
         this._refreshButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._refreshButtonClicked, this);
     }
@@ -43,7 +48,11 @@
 
     get navigationItems()
     {
-        return [this._refreshButtonNavigationItem];
+        let navigationItems = [];
+        if (this._setCookieButtonNavigationItem)
+            navigationItems.push(this._setCookieButtonNavigationItem);
+        navigationItems.push(this._refreshButtonNavigationItem);
+        return navigationItems;
     }
 
     saveToCookie(cookie)
@@ -111,6 +120,17 @@
         let contextMenu = WI.ContextMenu.createFromEvent(event);
 
         contextMenu.appendSeparator();
+
+        if (InspectorBackend.hasCommand("Page.setCookie")) {
+            contextMenu.appendItem(WI.UIString("Edit"), () => {
+                console.assert(!this._editingCookie);
+                this._editingCookie = this._cookies[rowIndex];
+
+                let popover = new WI.CookiePopover(this);
+                popover.show(this._editingCookie, this._table.cellForRowAndColumn(rowIndex, this._table.columns[0]), [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_X]);
+            });
+        }
+
         contextMenu.appendItem(WI.UIString("Copy"), () => {
             let rowIndexes;
             if (table.isRowSelected(rowIndex))
@@ -128,6 +148,7 @@
             else
                 table.removeRow(rowIndex);
         });
+
         contextMenu.appendSeparator();
     }
 
@@ -157,6 +178,18 @@
         return cell;
     }
 
+    // Popover delegate
+
+    willDismissPopover(popover)
+    {
+        if (popover instanceof WI.CookiePopover) {
+            this._willDismissCookiePopover(popover);
+            return;
+        }
+
+        console.assert();
+    }
+
     // Protected
 
     initialLayout()
@@ -315,6 +348,45 @@
         this._sortComparator = (a, b) => reverseFactor * comparator(a, b);
     }
 
+    async _willDismissCookiePopover(popover)
+    {
+        let editingCookie = this._editingCookie;
+        this._editingCookie = null;
+
+        let serializedData = popover.serializedData;
+        if (!serializedData) {
+            InspectorFrontendHost.beep();
+            return;
+        }
+
+        let cookieToSet = WI.Cookie.fromPayload(serializedData);
+
+        let cookieProtocolPayload = cookieToSet.toProtocol();
+        if (!cookieProtocolPayload) {
+            InspectorFrontendHost.beep();
+            return;
+        }
+
+        let target = WI.assumingMainTarget();
+
+        let promises = [];
+        if (editingCookie)
+            promises.push(target.PageAgent.deleteCookie(editingCookie.name, editingCookie.url));
+        promises.push(target.PageAgent.setCookie(cookieProtocolPayload));
+        promises.push(this._reloadCookies());
+        await Promise.all(promises);
+
+        let index = this._cookies.findIndex((existingCookie) => cookieToSet.equals(existingCookie));
+        if (index >= 0)
+            this._table.selectRow(index);
+    }
+
+    _handleSetCookieButtonClick(event)
+    {
+        let popover = new WI.CookiePopover(this);
+        popover.show(null, this._setCookieButtonNavigationItem.element, [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_X]);
+    }
+
     _refreshButtonClicked(event)
     {
         this._reloadCookies();
@@ -323,7 +395,7 @@
     _reloadCookies()
     {
         let target = WI.assumingMainTarget();
-        target.PageAgent.getCookies().then((payload) => {
+        return target.PageAgent.getCookies().then((payload) => {
             this._cookies = this._filterCookies(payload.cookies.map(WI.Cookie.fromPayload));
             this._updateSort();
             this._table.reloadData();
@@ -384,7 +456,7 @@
         case "path":
             return cookie.path || missingValue;
         case "expires":
-            return cookie.expires ? new Date(cookie.expires).toLocaleString() : WI.UIString("Session");
+            return (!cookie.session && cookie.expires) ? cookie.expires.toLocaleString() : WI.UIString("Session");
         case "size":
             return Number.bytesToString(cookie.size);
         case "secure":

Modified: trunk/Source/WebInspectorUI/UserInterface/Views/ResourceCookiesContentView.js (259172 => 259173)


--- trunk/Source/WebInspectorUI/UserInterface/Views/ResourceCookiesContentView.js	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/ResourceCookiesContentView.js	2020-03-29 03:07:52 UTC (rev 259173)
@@ -98,7 +98,7 @@
             cell.textContent = cookie.path || emDash;
             break;
         case "expires":
-            cell.textContent = cookie.expires ? cookie.expires.toLocaleString() : WI.UIString("Session");
+            cell.textContent = (!cookie.session && cookie.expires) ? cookie.expires.toLocaleString() : WI.UIString("Session");
             break;
         case "maxAge":
             cell.textContent = cookie.maxAge || emDash;

Modified: trunk/Source/WebKit/ChangeLog (259172 => 259173)


--- trunk/Source/WebKit/ChangeLog	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/Source/WebKit/ChangeLog	2020-03-29 03:07:52 UTC (rev 259173)
@@ -1,3 +1,20 @@
+2020-03-28  Devin Rousso  <[email protected]>
+
+        Web Inspector: support editing cookie key/values from inspector
+        https://bugs.webkit.org/show_bug.cgi?id=31157
+        <rdar://problem/19281523>
+
+        Reviewed by Timothy Hatcher.
+
+        * WebProcess/WebPage/WebCookieJar.h:
+        * WebProcess/WebPage/WebCookieJar.cpp:
+        (WebKit::WebCookieJar::setRawCookie):
+
+        * NetworkProcess/NetworkConnectionToWebProcess.messages.in:
+        * NetworkProcess/NetworkConnectionToWebProcess.h:
+        * NetworkProcess/NetworkConnectionToWebProcess.cpp:
+        (WebKit::NetworkConnectionToWebProcess::setRawCookie): Added.
+
 2020-03-28  David Kilzer  <[email protected]>
 
         REGRESSION (r258201): Use-after-move in UserMediaCaptureManager::Source::didFail()

Modified: trunk/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp (259172 => 259173)


--- trunk/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp	2020-03-29 03:07:52 UTC (rev 259173)
@@ -664,6 +664,15 @@
     completionHandler(WTFMove(result));
 }
 
+void NetworkConnectionToWebProcess::setRawCookie(const WebCore::Cookie& cookie)
+{
+    auto* networkStorageSession = storageSession();
+    if (!networkStorageSession)
+        return;
+
+    networkStorageSession->setCookie(cookie);
+}
+
 void NetworkConnectionToWebProcess::deleteCookie(const URL& url, const String& cookieName)
 {
     auto* networkStorageSession = storageSession();

Modified: trunk/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h (259172 => 259173)


--- trunk/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h	2020-03-29 03:07:52 UTC (rev 259173)
@@ -209,6 +209,7 @@
     void setCookiesFromDOM(const URL& firstParty, const WebCore::SameSiteInfo&, const URL&, WebCore::FrameIdentifier, WebCore::PageIdentifier, WebCore::ShouldAskITP, const String&);
     void cookieRequestHeaderFieldValue(const URL& firstParty, const WebCore::SameSiteInfo&, const URL&, Optional<WebCore::FrameIdentifier>, Optional<WebCore::PageIdentifier>, WebCore::IncludeSecureCookies, WebCore::ShouldAskITP, CompletionHandler<void(String cookieString, bool secureCookiesAccessed)>&&);
     void getRawCookies(const URL& firstParty, const WebCore::SameSiteInfo&, const URL&, Optional<WebCore::FrameIdentifier>, Optional<WebCore::PageIdentifier>, WebCore::ShouldAskITP, CompletionHandler<void(Vector<WebCore::Cookie>&&)>&&);
+    void setRawCookie(const WebCore::Cookie&);
     void deleteCookie(const URL&, const String& cookieName);
 
     void registerFileBlobURL(const URL&, const String& path, SandboxExtension::Handle&&, const String& contentType);

Modified: trunk/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in (259172 => 259173)


--- trunk/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in	2020-03-29 03:07:52 UTC (rev 259173)
@@ -39,6 +39,7 @@
     SetCookiesFromDOM(URL firstParty, struct WebCore::SameSiteInfo sameSiteInfo, URL url, WebCore::FrameIdentifier frameID, WebCore::PageIdentifier pageID, enum:bool WebCore::ShouldAskITP shouldAskITP, String cookieString)
     CookieRequestHeaderFieldValue(URL firstParty, struct WebCore::SameSiteInfo sameSiteInfo, URL url, Optional<WebCore::FrameIdentifier> frameID, Optional<WebCore::PageIdentifier> pageID, enum:bool WebCore::IncludeSecureCookies includeSecureCookies, enum:bool WebCore::ShouldAskITP shouldAskITP) -> (String cookieString, bool didAccessSecureCookies) Synchronous
     GetRawCookies(URL firstParty, struct WebCore::SameSiteInfo sameSiteInfo, URL url, Optional<WebCore::FrameIdentifier> frameID, Optional<WebCore::PageIdentifier> pageID, enum:bool WebCore::ShouldAskITP shouldAskITP) -> (Vector<WebCore::Cookie> cookies) Synchronous
+    SetRawCookie(struct WebCore::Cookie cookie)
     DeleteCookie(URL url, String cookieName)
     DomCookiesForHost(String host, bool subscribeToCookieChangeNotifications) -> (Vector<WebCore::Cookie> cookies) Synchronous
 #if HAVE(COOKIE_CHANGE_LISTENER_API)

Modified: trunk/Source/WebKit/WebProcess/WebPage/WebCookieJar.cpp (259172 => 259173)


--- trunk/Source/WebKit/WebProcess/WebPage/WebCookieJar.cpp	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/Source/WebKit/WebProcess/WebPage/WebCookieJar.cpp	2020-03-29 03:07:52 UTC (rev 259173)
@@ -228,6 +228,11 @@
     return true;
 }
 
+void WebCookieJar::setRawCookie(const WebCore::Document& document, const Cookie& cookie)
+{
+    WebProcess::singleton().ensureNetworkProcessConnection().connection().send(Messages::NetworkConnectionToWebProcess::SetRawCookie(cookie), 0);
+}
+
 void WebCookieJar::deleteCookie(const WebCore::Document& document, const URL& url, const String& cookieName)
 {
     WebProcess::singleton().ensureNetworkProcessConnection().connection().send(Messages::NetworkConnectionToWebProcess::DeleteCookie(url, cookieName), 0);

Modified: trunk/Source/WebKit/WebProcess/WebPage/WebCookieJar.h (259172 => 259173)


--- trunk/Source/WebKit/WebProcess/WebPage/WebCookieJar.h	2020-03-29 03:02:59 UTC (rev 259172)
+++ trunk/Source/WebKit/WebProcess/WebPage/WebCookieJar.h	2020-03-29 03:07:52 UTC (rev 259173)
@@ -45,6 +45,7 @@
     bool cookiesEnabled(const WebCore::Document&) const final;
     std::pair<String, WebCore::SecureCookiesAccessed> cookieRequestHeaderFieldValue(const URL& firstParty, const WebCore::SameSiteInfo&, const URL&, Optional<WebCore::FrameIdentifier>, Optional<WebCore::PageIdentifier>, WebCore::IncludeSecureCookies) const final;
     bool getRawCookies(const WebCore::Document&, const URL&, Vector<WebCore::Cookie>&) const final;
+    void setRawCookie(const WebCore::Document&, const WebCore::Cookie&) final;
     void deleteCookie(const WebCore::Document&, const URL&, const String& cookieName) final;
 
     void cookiesAdded(const String& host, const Vector<WebCore::Cookie>&);
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to