feat: context.*cookies (#42)
This commit is contained in:
@@ -77,9 +77,13 @@ class TypeRef extends Element {
|
||||
|
||||
void createCustomType() {
|
||||
boolean isEnum = jsonName.contains("|\"");
|
||||
boolean isClass = jsonName.replace("null|", "").equals("Object");
|
||||
boolean isClass = jsonName.replace("null|", "").equals("Object")
|
||||
|| jsonName.equals("Promise<Array<Object>>");
|
||||
// Use path to the corresponding method, param of field as the key.
|
||||
String parentPath = parent.jsonPath;
|
||||
if (jsonName.equals("Array<Object>") && "BrowserContext.addCookies.cookies".equals(jsonPath)) {
|
||||
isClass = true;
|
||||
}
|
||||
Types.Mapping mapping = TypeDefinition.types.findForPath(parentPath);
|
||||
if (mapping == null) {
|
||||
if (isEnum) {
|
||||
@@ -266,6 +270,14 @@ class Method extends Element {
|
||||
"void unroute(Pattern url, BiConsumer<Route, Request> handler);",
|
||||
"void unroute(Predicate<String> url, BiConsumer<Route, Request> handler);",
|
||||
});
|
||||
customSignature.put("BrowserContext.cookies", new String[]{
|
||||
"default List<Cookie> cookies() { return cookies((List<String>) null); }",
|
||||
"default List<Cookie> cookies(String url) { return cookies(Arrays.asList(url)); }",
|
||||
"List<Cookie> cookies(List<String> urls);",
|
||||
});
|
||||
customSignature.put("BrowserContext.addCookies", new String[]{
|
||||
"void addCookies(List<AddCookie> cookies);"
|
||||
});
|
||||
customSignature.put("FileChooser.setFiles", new String[]{
|
||||
"default void setFiles(File file) { setFiles(file, null); }",
|
||||
"default void setFiles(File file, SetFilesOptions options) { setFiles(new File[]{ file }, options); }",
|
||||
@@ -706,6 +718,8 @@ class Interface extends TypeDefinition {
|
||||
break;
|
||||
}
|
||||
case "BrowserContext": {
|
||||
output.add(offset + "enum SameSite { STRICT, LAX, NONE }");
|
||||
output.add("");
|
||||
output.add(offset + "class HTTPCredentials {");
|
||||
output.add(offset + " private final String username;");
|
||||
output.add(offset + " private final String password;");
|
||||
|
||||
@@ -208,7 +208,12 @@ class Types {
|
||||
add("Page.goto.options", "Object", "NavigateOptions");
|
||||
add("Frame.goto.options", "Object", "NavigateOptions");
|
||||
|
||||
add("BrowserContext.cookies.urls", "string|Array<string>", "String");
|
||||
// The method has custom signatures
|
||||
add("BrowserContext.cookies", "Promise<Array<Object>>", "Cookie");
|
||||
add("BrowserContext.cookies.sameSite", "\"Lax\"|\"None\"|\"Strict\"", "SameSite", new Empty());
|
||||
add("BrowserContext.cookies.expires", "number", "long");
|
||||
add("BrowserContext.addCookies.cookies", "Array<Object>", "AddCookie");
|
||||
add("BrowserContext.addCookies.cookies.sameSite", "\"Lax\"|\"None\"|\"Strict\"", "SameSite", new Empty());
|
||||
add("BrowserContext.route.url", "string|RegExp|function(URL):boolean", "String");
|
||||
add("BrowserContext.unroute.url", "string|RegExp|function(URL):boolean", "String");
|
||||
add("BrowserContext.waitForEvent.event", "string", "EventType", new Empty());
|
||||
|
||||
@@ -22,6 +22,8 @@ import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public interface BrowserContext {
|
||||
enum SameSite { STRICT, LAX, NONE }
|
||||
|
||||
class HTTPCredentials {
|
||||
private final String username;
|
||||
private final String password;
|
||||
@@ -59,6 +61,89 @@ public interface BrowserContext {
|
||||
|
||||
void addListener(EventType type, Listener<EventType> listener);
|
||||
void removeListener(EventType type, Listener<EventType> listener);
|
||||
class AddCookie {
|
||||
public String name;
|
||||
public String value;
|
||||
public String url;
|
||||
public String domain;
|
||||
public String path;
|
||||
public Integer expires;
|
||||
public Boolean httpOnly;
|
||||
public Boolean secure;
|
||||
public SameSite sameSite;
|
||||
|
||||
public AddCookie withName(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
public AddCookie withValue(String value) {
|
||||
this.value = value;
|
||||
return this;
|
||||
}
|
||||
public AddCookie withUrl(String url) {
|
||||
this.url = url;
|
||||
return this;
|
||||
}
|
||||
public AddCookie withDomain(String domain) {
|
||||
this.domain = domain;
|
||||
return this;
|
||||
}
|
||||
public AddCookie withPath(String path) {
|
||||
this.path = path;
|
||||
return this;
|
||||
}
|
||||
public AddCookie withExpires(Integer expires) {
|
||||
this.expires = expires;
|
||||
return this;
|
||||
}
|
||||
public AddCookie withHttpOnly(Boolean httpOnly) {
|
||||
this.httpOnly = httpOnly;
|
||||
return this;
|
||||
}
|
||||
public AddCookie withSecure(Boolean secure) {
|
||||
this.secure = secure;
|
||||
return this;
|
||||
}
|
||||
public AddCookie withSameSite(SameSite sameSite) {
|
||||
this.sameSite = sameSite;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
class Cookie {
|
||||
private String name;
|
||||
private String value;
|
||||
private String domain;
|
||||
private String path;
|
||||
private long expires;
|
||||
private boolean httpOnly;
|
||||
private boolean secure;
|
||||
private SameSite sameSite;
|
||||
|
||||
public String name() {
|
||||
return this.name;
|
||||
}
|
||||
public String value() {
|
||||
return this.value;
|
||||
}
|
||||
public String domain() {
|
||||
return this.domain;
|
||||
}
|
||||
public String path() {
|
||||
return this.path;
|
||||
}
|
||||
public long expires() {
|
||||
return this.expires;
|
||||
}
|
||||
public boolean httpOnly() {
|
||||
return this.httpOnly;
|
||||
}
|
||||
public boolean secure() {
|
||||
return this.secure;
|
||||
}
|
||||
public SameSite sameSite() {
|
||||
return this.sameSite;
|
||||
}
|
||||
}
|
||||
class ExposeBindingOptions {
|
||||
public Boolean handle;
|
||||
|
||||
@@ -94,7 +179,7 @@ public interface BrowserContext {
|
||||
}
|
||||
}
|
||||
void close();
|
||||
void addCookies(List<Object> cookies);
|
||||
void addCookies(List<AddCookie> cookies);
|
||||
default void addInitScript(String script) {
|
||||
addInitScript(script, null);
|
||||
}
|
||||
@@ -102,10 +187,9 @@ public interface BrowserContext {
|
||||
Browser browser();
|
||||
void clearCookies();
|
||||
void clearPermissions();
|
||||
default List<Object> cookies() {
|
||||
return cookies(null);
|
||||
}
|
||||
List<Object> cookies(String urls);
|
||||
default List<Cookie> cookies() { return cookies((List<String>) null); }
|
||||
default List<Cookie> cookies(String url) { return cookies(Arrays.asList(url)); }
|
||||
List<Cookie> cookies(List<String> urls);
|
||||
default void exposeBinding(String name, Page.Binding playwrightBinding) {
|
||||
exposeBinding(name, playwrightBinding, null);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,40 @@ public interface ChromiumCoverage {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
class ChromiumCoverageStopCSSCoverage {
|
||||
private String url;
|
||||
private String text;
|
||||
private List<Object> ranges;
|
||||
|
||||
public String url() {
|
||||
return this.url;
|
||||
}
|
||||
public String text() {
|
||||
return this.text;
|
||||
}
|
||||
public List<Object> ranges() {
|
||||
return this.ranges;
|
||||
}
|
||||
}
|
||||
class ChromiumCoverageStopJSCoverage {
|
||||
private String url;
|
||||
private String scriptId;
|
||||
private String source;
|
||||
private List<Object> functions;
|
||||
|
||||
public String url() {
|
||||
return this.url;
|
||||
}
|
||||
public String scriptId() {
|
||||
return this.scriptId;
|
||||
}
|
||||
public String source() {
|
||||
return this.source;
|
||||
}
|
||||
public List<Object> functions() {
|
||||
return this.functions;
|
||||
}
|
||||
}
|
||||
default void startCSSCoverage() {
|
||||
startCSSCoverage(null);
|
||||
}
|
||||
@@ -48,7 +82,7 @@ public interface ChromiumCoverage {
|
||||
startJSCoverage(null);
|
||||
}
|
||||
void startJSCoverage(StartJSCoverageOptions options);
|
||||
List<Object> stopCSSCoverage();
|
||||
List<Object> stopJSCoverage();
|
||||
ChromiumCoverageStopCSSCoverage stopCSSCoverage();
|
||||
ChromiumCoverageStopJSCoverage stopJSCoverage();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,15 +16,15 @@
|
||||
|
||||
package com.microsoft.playwright.impl;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonToken;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import com.microsoft.playwright.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -67,8 +67,10 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCookies(List<Object> cookies) {
|
||||
|
||||
public void addCookies(List<AddCookie> cookies) {
|
||||
JsonObject params = new JsonObject();
|
||||
params.add("cookies", new Gson().toJsonTree(cookies));
|
||||
sendMessage("addCookies", params);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -89,17 +91,52 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
|
||||
|
||||
@Override
|
||||
public void clearCookies() {
|
||||
|
||||
sendMessage("clearCookies");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearPermissions() {
|
||||
sendMessage("clearPermissions");
|
||||
}
|
||||
|
||||
private static class SameSiteAdapter extends TypeAdapter<SameSite> {
|
||||
@Override
|
||||
public void write(JsonWriter out, SameSite value) throws IOException {
|
||||
String stringValue;
|
||||
switch (value) {
|
||||
case STRICT:
|
||||
stringValue = "Strict";
|
||||
break;
|
||||
case LAX:
|
||||
stringValue = "Lax";
|
||||
break;
|
||||
case NONE:
|
||||
stringValue = "None";
|
||||
break;
|
||||
default:
|
||||
throw new PlaywrightException("Unexpected value: " + value);
|
||||
}
|
||||
out.value(stringValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SameSite read(JsonReader in) throws IOException {
|
||||
String value = in.nextString();
|
||||
return SameSite.valueOf(value.toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Object> cookies(String urls) {
|
||||
return null;
|
||||
public List<Cookie> cookies(List<String> urls) {
|
||||
JsonObject params = new JsonObject();
|
||||
if (urls == null) {
|
||||
urls = Collections.emptyList();
|
||||
}
|
||||
params.add("urls", new Gson().toJsonTree(urls));
|
||||
JsonObject json = sendMessage("cookies", params).getAsJsonObject();
|
||||
Gson gson = new GsonBuilder().registerTypeAdapter(SameSite.class, new SameSiteAdapter().nullSafe()).create();
|
||||
Cookie[] cookies = gson.fromJson(json.getAsJsonArray("cookies"), Cookie[].class);
|
||||
return Arrays.asList(cookies);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.microsoft.playwright;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class TestBrowserContextClearCookies extends TestBase {
|
||||
@Test
|
||||
void shouldClearCookies() {
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
context.addCookies(asList(
|
||||
new BrowserContext.AddCookie().withUrl(server.EMPTY_PAGE).withName("cookie1").withValue("1")));
|
||||
assertEquals("cookie1=1", page.evaluate("document.cookie"));
|
||||
context.clearCookies();
|
||||
assertEquals(emptyList(), context.cookies());
|
||||
page.reload();
|
||||
assertEquals("", page.evaluate("document.cookie"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIsolateCookiesWhenClearing() {
|
||||
BrowserContext anotherContext = browser.newContext();
|
||||
context.addCookies(asList(
|
||||
new BrowserContext.AddCookie().withUrl(server.EMPTY_PAGE).withName("page1cookie").withValue("page1value")));
|
||||
anotherContext.addCookies(asList(
|
||||
new BrowserContext.AddCookie().withUrl(server.EMPTY_PAGE).withName("page2cookie").withValue("page2value")));
|
||||
|
||||
assertEquals(1, (context.cookies()).size());
|
||||
assertEquals(1, (anotherContext.cookies()).size());
|
||||
|
||||
context.clearCookies();
|
||||
assertEquals(0, (context.cookies()).size());
|
||||
assertEquals(1, (anotherContext.cookies()).size());
|
||||
|
||||
anotherContext.clearCookies();
|
||||
assertEquals(0, (context.cookies()).size());
|
||||
assertEquals(0, (anotherContext.cookies()).size());
|
||||
anotherContext.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.microsoft.playwright;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class TestBrowserContextCookies extends TestBase {
|
||||
private static void assertJsonEquals(String expected, Object actual) {
|
||||
JsonElement actualJson = JsonParser.parseString(new Gson().toJson(actual));
|
||||
assertEquals(JsonParser.parseString(expected), actualJson);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetACookie() {
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
Object documentCookie = page.evaluate("() => {\n" +
|
||||
" document.cookie = 'username=John Doe';\n" +
|
||||
" return document.cookie;\n" +
|
||||
"}");
|
||||
assertEquals("username=John Doe", documentCookie);
|
||||
List<BrowserContext.Cookie> cookies = context.cookies();
|
||||
assertJsonEquals("[{\n" +
|
||||
" name: 'username',\n" +
|
||||
" value: 'John Doe',\n" +
|
||||
" domain: 'localhost',\n" +
|
||||
" path: '/',\n" +
|
||||
" expires: -1,\n" +
|
||||
" httpOnly: false,\n" +
|
||||
" secure: false,\n" +
|
||||
" sameSite: 'NONE'\n" +
|
||||
" }]", cookies);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldGetANonSessionCookie() {
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
// @see https://en.wikipedia.org/wiki/Year_2038_problem
|
||||
Object documentCookie = page.evaluate("() => {\n" +
|
||||
" const date= new Date('1/1/2038');\n" +
|
||||
" document.cookie = `username=John Doe;expires=${date.toUTCString()}`;\n" +
|
||||
" return document.cookie;\n" +
|
||||
" }");
|
||||
assertEquals("username=John Doe", documentCookie);
|
||||
int timestamp = (Integer) page.evaluate("+(new Date('1/1/2038'))/1000");
|
||||
BrowserContext.Cookie cookie = context.cookies().get(0);
|
||||
assertEquals("username", cookie.name());
|
||||
assertEquals("John Doe", cookie.value());
|
||||
assertEquals("localhost", cookie.domain());
|
||||
assertEquals("/", cookie.path());
|
||||
assertEquals(timestamp, cookie.expires());
|
||||
assertEquals(false, cookie.httpOnly());
|
||||
assertEquals(false, cookie.secure());
|
||||
assertEquals(BrowserContext.SameSite.NONE, cookie.sameSite());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProperlyReportHttpOnlyCookie() {
|
||||
server.setRoute("/empty.html", exchange -> {
|
||||
exchange.getResponseHeaders().add("Set-Cookie", "name=value;HttpOnly; Path=/");
|
||||
exchange.sendResponseHeaders(200, 0);
|
||||
exchange.getResponseBody().close();
|
||||
});
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
List<BrowserContext.Cookie> cookies = context.cookies();
|
||||
assertEquals(1, cookies.size());
|
||||
assertTrue(cookies.get(0).httpOnly());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProperlyReportStrictSameSiteCookie() {
|
||||
// TODO: test.fail(browserName === "webkit" && platform === "win32");
|
||||
server.setRoute("/empty.html", exchange -> {
|
||||
exchange.getResponseHeaders().add("Set-Cookie", "name=value;SameSite=Strict");
|
||||
exchange.sendResponseHeaders(200, 0);
|
||||
exchange.getResponseBody().close();
|
||||
});
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
List<BrowserContext.Cookie> cookies = context.cookies();
|
||||
assertEquals(1, cookies.size());
|
||||
assertEquals(BrowserContext.SameSite.STRICT, cookies.get(0).sameSite());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProperlyReportLaxSameSiteCookie() {
|
||||
// TODO: test.fail(browserName === "webkit" && platform === "win32");
|
||||
server.setRoute("/empty.html", exchange -> {
|
||||
exchange.getResponseHeaders().add("Set-Cookie", "name=value;SameSite=Lax");
|
||||
exchange.sendResponseHeaders(200, 0);
|
||||
exchange.getResponseBody().close();
|
||||
});
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
List<BrowserContext.Cookie> cookies = context.cookies();
|
||||
assertEquals(1, cookies.size());
|
||||
assertEquals(BrowserContext.SameSite.LAX, cookies.get(0).sameSite());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetMultipleCookies() {
|
||||
page.navigate(server.EMPTY_PAGE);
|
||||
Object documentCookie = page.evaluate("() => {\n" +
|
||||
" document.cookie = 'username=John Doe';\n" +
|
||||
" document.cookie = 'password=1234';\n" +
|
||||
" return document.cookie.split('; ').sort().join('; ');\n" +
|
||||
"}");
|
||||
List<BrowserContext.Cookie> cookies = context.cookies();
|
||||
cookies.sort(Comparator.comparing(BrowserContext.Cookie::name));
|
||||
assertEquals("password=1234; username=John Doe", documentCookie);
|
||||
assertJsonEquals("[\n" +
|
||||
" {\n" +
|
||||
" name: 'password',\n" +
|
||||
" value: '1234',\n" +
|
||||
" domain: 'localhost',\n" +
|
||||
" path: '/',\n" +
|
||||
" expires: -1,\n" +
|
||||
" httpOnly: false,\n" +
|
||||
" secure: false,\n" +
|
||||
" sameSite: 'NONE'\n" +
|
||||
" },\n" +
|
||||
" {\n" +
|
||||
" name: 'username',\n" +
|
||||
" value: 'John Doe',\n" +
|
||||
" domain: 'localhost',\n" +
|
||||
" path: '/',\n" +
|
||||
" expires: -1,\n" +
|
||||
" httpOnly: false,\n" +
|
||||
" secure: false,\n" +
|
||||
" sameSite: 'NONE'\n" +
|
||||
" }\n" +
|
||||
"]", cookies);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetCookiesFromMultipleUrls() {
|
||||
context.addCookies(asList(
|
||||
new BrowserContext.AddCookie().withUrl("https://foo.com").withName("doggo").withValue("woofs"),
|
||||
new BrowserContext.AddCookie().withUrl("https://bar.com").withName("catto").withValue("purrs"),
|
||||
new BrowserContext.AddCookie().withUrl("https://baz.com").withName("birdo").withValue("tweets")));
|
||||
List<BrowserContext.Cookie> cookies = context.cookies(asList("https://foo.com", "https://baz.com"));
|
||||
cookies.sort(Comparator.comparing(BrowserContext.Cookie::name));
|
||||
assertJsonEquals("[{\n" +
|
||||
" name: 'birdo',\n" +
|
||||
" value: 'tweets',\n" +
|
||||
" domain: 'baz.com',\n" +
|
||||
" path: '/',\n" +
|
||||
" expires: -1,\n" +
|
||||
" httpOnly: false,\n" +
|
||||
" secure: true,\n" +
|
||||
" sameSite: 'NONE'\n" +
|
||||
"}, {\n" +
|
||||
" name: 'doggo',\n" +
|
||||
" value: 'woofs',\n" +
|
||||
" domain: 'foo.com',\n" +
|
||||
" path: '/',\n" +
|
||||
" expires: -1,\n" +
|
||||
" httpOnly: false,\n" +
|
||||
" secure: true,\n" +
|
||||
" sameSite: 'NONE'\n" +
|
||||
"}]", cookies);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user