chore(rpc): misc serializer improvements (#2832)
This commit is contained in:
parent
7f60c4df62
commit
2540805bf2
|
|
@ -362,15 +362,7 @@ class InterceptableRequest implements network.RouteDelegate {
|
|||
}
|
||||
|
||||
async fulfill(response: types.NormalizedFulfillResponse) {
|
||||
const responseBody = response.body && helper.isString(response.body) ? Buffer.from(response.body) : (response.body || null);
|
||||
|
||||
const responseHeaders: { [s: string]: string; } = {};
|
||||
for (const header of Object.keys(response.headers))
|
||||
responseHeaders[header.toLowerCase()] = response.headers[header];
|
||||
if (response.contentType)
|
||||
responseHeaders['content-type'] = response.contentType;
|
||||
if (responseBody && !('content-length' in responseHeaders))
|
||||
responseHeaders['content-length'] = String(Buffer.byteLength(responseBody));
|
||||
const body = response.isBase64 ? response.body : Buffer.from(response.body).toString('base64');
|
||||
|
||||
// In certain cases, protocol will return error if the request was already canceled
|
||||
// or the page was closed. We should tolerate these errors.
|
||||
|
|
@ -378,8 +370,8 @@ class InterceptableRequest implements network.RouteDelegate {
|
|||
requestId: this._interceptionId!,
|
||||
responseCode: response.status,
|
||||
responsePhrase: network.STATUS_TEXTS[String(response.status)],
|
||||
responseHeaders: headersArray(responseHeaders),
|
||||
body: responseBody ? responseBody.toString('base64') : undefined,
|
||||
responseHeaders: headersArray(response.headers),
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -173,22 +173,14 @@ class InterceptableRequest implements network.RouteDelegate {
|
|||
}
|
||||
|
||||
async fulfill(response: types.NormalizedFulfillResponse) {
|
||||
const responseBody = response.body && helper.isString(response.body) ? Buffer.from(response.body) : (response.body || null);
|
||||
|
||||
const responseHeaders: { [s: string]: string; } = {};
|
||||
for (const header of Object.keys(response.headers))
|
||||
responseHeaders[header.toLowerCase()] = response.headers[header];
|
||||
if (response.contentType)
|
||||
responseHeaders['content-type'] = response.contentType;
|
||||
if (responseBody && !('content-length' in responseHeaders))
|
||||
responseHeaders['content-length'] = String(Buffer.byteLength(responseBody));
|
||||
const base64body = response.isBase64 ? response.body : Buffer.from(response.body).toString('base64');
|
||||
|
||||
await this._session.sendMayFail('Network.fulfillInterceptedRequest', {
|
||||
requestId: this._id,
|
||||
status: response.status,
|
||||
statusText: network.STATUS_TEXTS[String(response.status)] || '',
|
||||
headers: headersArray(responseHeaders),
|
||||
base64body: responseBody ? responseBody.toString('base64') : undefined,
|
||||
headers: headersArray(response.headers),
|
||||
base64body,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -148,8 +148,8 @@ export type PageInitializer = {
|
|||
|
||||
|
||||
export interface FrameChannel extends Channel {
|
||||
$$eval(params: { selector: string; expression: string, isFunction: boolean, arg: any, isPage?: boolean }): Promise<any>;
|
||||
$eval(params: { selector: string; expression: string, isFunction: boolean, arg: any, isPage?: boolean }): Promise<any>;
|
||||
evalOnSelector(params: { selector: string; expression: string, isFunction: boolean, arg: any, isPage?: boolean }): Promise<any>;
|
||||
evalOnSelectorAll(params: { selector: string; expression: string, isFunction: boolean, arg: any, isPage?: boolean }): Promise<any>;
|
||||
addScriptTag(params: { options: { url?: string | undefined, path?: string | undefined, content?: string | undefined, type?: string | undefined }, isPage?: boolean }): Promise<ElementHandleChannel>;
|
||||
addStyleTag(params: { options: { url?: string | undefined, path?: string | undefined, content?: string | undefined }, isPage?: boolean }): Promise<ElementHandleChannel>;
|
||||
check(params: { selector: string, options: types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean }, isPage?: boolean }): Promise<void>;
|
||||
|
|
@ -205,6 +205,7 @@ export interface JSHandleChannel extends Channel {
|
|||
evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise<any>;
|
||||
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise<JSHandleChannel>;
|
||||
getPropertyList(): Promise<{ name: string, value: JSHandleChannel}[]>;
|
||||
getProperty(params: { name: string }): Promise<JSHandleChannel>;
|
||||
jsonValue(): Promise<any>;
|
||||
}
|
||||
export type JSHandleInitializer = {
|
||||
|
|
@ -213,8 +214,8 @@ export type JSHandleInitializer = {
|
|||
|
||||
|
||||
export interface ElementHandleChannel extends JSHandleChannel {
|
||||
$$evalExpression(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise<any>;
|
||||
$evalExpression(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise<any>;
|
||||
evalOnSelector(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise<any>;
|
||||
evalOnSelectorAll(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise<any>;
|
||||
boundingBox(): Promise<types.Rect | null>;
|
||||
check(params: { options?: types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise<void>;
|
||||
click(params: { options?: types.PointerActionOptions & types.MouseClickOptions & types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise<void>;
|
||||
|
|
@ -264,8 +265,8 @@ export interface RouteChannel extends Channel {
|
|||
response: {
|
||||
status?: number,
|
||||
headers?: types.Headers,
|
||||
contentType?: string,
|
||||
body: Binary,
|
||||
body: string,
|
||||
isBase64: boolean,
|
||||
}
|
||||
}): Promise<void>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
|
|||
this._browser._contexts.delete(this);
|
||||
|
||||
for (const [listener, event] of this._pendingWaitForEvents) {
|
||||
if (event === Events.Page.Close)
|
||||
if (event === Events.BrowserContext.Close)
|
||||
continue;
|
||||
listener(new Error('Context closed'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,8 +84,7 @@ export class Connection {
|
|||
|
||||
debug('pw:channel:event')(parsedMessage);
|
||||
if (method === '__create__') {
|
||||
const scopeObject = this._objects.get(guid);
|
||||
const scope = scopeObject ? scopeObject._scope : this._rootScript;
|
||||
const scope = this._scopes.get(guid)!;
|
||||
scope.createRemoteObject(params.type, params.guid, params.initializer);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,13 +139,13 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
|
|||
async $eval<R, Arg>(selector: string, pageFunction: FuncOn<Element, Arg, R>, arg: Arg): Promise<R>;
|
||||
async $eval<R>(selector: string, pageFunction: FuncOn<Element, void, R>, arg?: any): Promise<R>;
|
||||
async $eval<R, Arg>(selector: string, pageFunction: FuncOn<Element, Arg, R>, arg: Arg): Promise<R> {
|
||||
return parseResult(await this._elementChannel.$evalExpression({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }));
|
||||
return parseResult(await this._elementChannel.evalOnSelector({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }));
|
||||
}
|
||||
|
||||
async $$eval<R, Arg>(selector: string, pageFunction: FuncOn<Element[], Arg, R>, arg: Arg): Promise<R>;
|
||||
async $$eval<R>(selector: string, pageFunction: FuncOn<Element[], void, R>, arg?: any): Promise<R>;
|
||||
async $$eval<R, Arg>(selector: string, pageFunction: FuncOn<Element[], Arg, R>, arg: Arg): Promise<R> {
|
||||
return parseResult(await this._elementChannel.$$evalExpression({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }));
|
||||
return parseResult(await this._elementChannel.evalOnSelectorAll({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,14 +105,14 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
|
|||
async $eval<R>(selector: string, pageFunction: FuncOn<Element, void, R>, arg?: any): Promise<R>;
|
||||
async $eval<R, Arg>(selector: string, pageFunction: FuncOn<Element, Arg, R>, arg: Arg): Promise<R> {
|
||||
assertMaxArguments(arguments.length, 3);
|
||||
return await this._channel.$eval({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg), isPage: this._page!._isPageCall });
|
||||
return await this._channel.evalOnSelector({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg), isPage: this._page!._isPageCall });
|
||||
}
|
||||
|
||||
async $$eval<R, Arg>(selector: string, pageFunction: FuncOn<Element[], Arg, R>, arg: Arg): Promise<R>;
|
||||
async $$eval<R>(selector: string, pageFunction: FuncOn<Element[], void, R>, arg?: any): Promise<R>;
|
||||
async $$eval<R, Arg>(selector: string, pageFunction: FuncOn<Element[], Arg, R>, arg: Arg): Promise<R> {
|
||||
assertMaxArguments(arguments.length, 3);
|
||||
return await this._channel.$$eval({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg), isPage: this._page!._isPageCall });
|
||||
return await this._channel.evalOnSelectorAll({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg), isPage: this._page!._isPageCall });
|
||||
}
|
||||
|
||||
async $$(selector: string): Promise<ElementHandle<Element>[]> {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export class Keyboard {
|
|||
await this._channel.keyboardInsertText({ text });
|
||||
}
|
||||
|
||||
async type(text: string, options?: { delay?: number }) {
|
||||
async type(text: string, options: { delay?: number } = {}) {
|
||||
await this._channel.keyboardType({ text, options });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,20 +62,13 @@ export class JSHandle<T = any> extends ChannelOwner<JSHandleChannel, JSHandleIni
|
|||
async evaluateHandle<R, Arg>(pageFunction: FuncOn<T, Arg, R>, arg: Arg): Promise<SmartHandle<R>>;
|
||||
async evaluateHandle<R>(pageFunction: FuncOn<T, void, R>, arg?: any): Promise<SmartHandle<R>>;
|
||||
async evaluateHandle<R, Arg>(pageFunction: FuncOn<T, Arg, R>, arg: Arg): Promise<SmartHandle<R>> {
|
||||
const handleChannel = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });
|
||||
const handleChannel = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });
|
||||
return JSHandle.from(handleChannel) as SmartHandle<R>;
|
||||
}
|
||||
|
||||
async getProperty(propertyName: string): Promise<JSHandle> {
|
||||
const objectHandle = await this.evaluateHandle((object: any, propertyName: string) => {
|
||||
const result: any = {__proto__: null};
|
||||
result[propertyName] = object[propertyName];
|
||||
return result;
|
||||
}, propertyName);
|
||||
const properties = await objectHandle.getProperties();
|
||||
const result = properties.get(propertyName)!;
|
||||
objectHandle.dispose();
|
||||
return result;
|
||||
async getProperty(name: string): Promise<JSHandle> {
|
||||
const handleChannel = await this._channel.getProperty({ name });
|
||||
return JSHandle.from(handleChannel);
|
||||
}
|
||||
|
||||
async getProperties(): Promise<Map<string, JSHandle>> {
|
||||
|
|
|
|||
|
|
@ -152,12 +152,7 @@ export class Route extends ChannelOwner<RouteChannel, RouteInitializer> {
|
|||
|
||||
async fulfill(response: types.FulfillResponse & { path?: string }) {
|
||||
const normalized = await normalizeFulfillParameters(response);
|
||||
await this._channel.fulfill({ response: {
|
||||
status: normalized.status,
|
||||
headers: normalized.headers,
|
||||
contentType: normalized.contentType,
|
||||
body: (typeof normalized.body === 'string' ? Buffer.from(normalized.body) : normalized.body).toString('base64')
|
||||
}});
|
||||
await this._channel.fulfill({ response: normalized });
|
||||
}
|
||||
|
||||
async continue(overrides: { method?: string; headers?: types.Headers; postData?: string } = {}) {
|
||||
|
|
|
|||
|
|
@ -144,8 +144,10 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
|
|||
|
||||
async _onBinding(bindingCall: BindingCall) {
|
||||
const func = this._bindings.get(bindingCall._initializer.name);
|
||||
if (func)
|
||||
if (func) {
|
||||
bindingCall.call(func);
|
||||
return;
|
||||
}
|
||||
this._browserContext!._onBinding(bindingCall);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import * as path from 'path';
|
|||
import * as util from 'util';
|
||||
import { TimeoutError } from '../errors';
|
||||
import * as types from '../types';
|
||||
import { helper } from '../helper';
|
||||
|
||||
|
||||
export function serializeError(e: any): types.Error {
|
||||
|
|
@ -64,18 +65,37 @@ export async function normalizeFilePayloads(files: string | types.FilePayload |
|
|||
}
|
||||
|
||||
export async function normalizeFulfillParameters(params: types.FulfillResponse & { path?: string }): Promise<types.NormalizedFulfillResponse> {
|
||||
let body = '';
|
||||
let isBase64 = false;
|
||||
let length = 0;
|
||||
if (params.path) {
|
||||
return {
|
||||
status: params.status || 200,
|
||||
headers: params.headers || {},
|
||||
contentType: mime.getType(params.path) || 'application/octet-stream',
|
||||
body: await util.promisify(fs.readFile)(params.path)
|
||||
};
|
||||
const buffer = await util.promisify(fs.readFile)(params.path);
|
||||
body = buffer.toString('base64');
|
||||
isBase64 = true;
|
||||
length = buffer.length;
|
||||
} else if (helper.isString(params.body)) {
|
||||
body = params.body;
|
||||
isBase64 = false;
|
||||
length = Buffer.byteLength(body);
|
||||
} else if (params.body) {
|
||||
body = params.body.toString('base64');
|
||||
isBase64 = true;
|
||||
length = params.body.length;
|
||||
}
|
||||
const headers: { [s: string]: string; } = {};
|
||||
for (const header of Object.keys(params.headers || {}))
|
||||
headers[header.toLowerCase()] = String(params.headers![header]);
|
||||
if (params.contentType)
|
||||
headers['content-type'] = String(params.contentType);
|
||||
else if (params.path)
|
||||
headers['content-type'] = mime.getType(params.path) || 'application/octet-stream';
|
||||
if (length && !('content-length' in headers))
|
||||
headers['content-length'] = String(length);
|
||||
|
||||
return {
|
||||
status: params.status || 200,
|
||||
headers: params.headers || {},
|
||||
contentType: params.contentType,
|
||||
body: params.body || ''
|
||||
headers,
|
||||
body,
|
||||
isBase64
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,11 +139,11 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements Eleme
|
|||
return elements.map(e => new ElementHandleDispatcher(this._scope, e));
|
||||
}
|
||||
|
||||
async $evalExpression(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise<any> {
|
||||
async evalOnSelector(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise<any> {
|
||||
return serializeResult(await this._elementHandle._$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg)));
|
||||
}
|
||||
|
||||
async $$evalExpression(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise<any> {
|
||||
async evalOnSelectorAll(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise<any> {
|
||||
return serializeResult(await this._elementHandle._$$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,12 +78,12 @@ export class FrameDispatcher extends Dispatcher<Frame, FrameInitializer> impleme
|
|||
return target.dispatchEvent(params.selector, params.type, parseArgument(params.eventInit), params.options);
|
||||
}
|
||||
|
||||
async $eval(params: { selector: string, expression: string, isFunction: boolean, arg: any, isPage?: boolean }): Promise<any> {
|
||||
async evalOnSelector(params: { selector: string, expression: string, isFunction: boolean, arg: any, isPage?: boolean }): Promise<any> {
|
||||
const target = params.isPage ? this._frame._page : this._frame;
|
||||
return serializeResult(await target._$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg)));
|
||||
}
|
||||
|
||||
async $$eval(params: { selector: string, expression: string, isFunction: boolean, arg: any, isPage?: boolean }): Promise<any> {
|
||||
async evalOnSelectorAll(params: { selector: string, expression: string, isFunction: boolean, arg: any, isPage?: boolean }): Promise<any> {
|
||||
const target = params.isPage ? this._frame._page : this._frame;
|
||||
return serializeResult(await target._$$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@ export class JSHandleDispatcher extends Dispatcher<js.JSHandle, JSHandleInitiali
|
|||
return createHandle(this._scope, jsHandle);
|
||||
}
|
||||
|
||||
async getProperty(params: { name: string }): Promise<JSHandleChannel> {
|
||||
const jsHandle = await this._object.getProperty(params.name);
|
||||
return createHandle(this._scope, jsHandle);
|
||||
}
|
||||
|
||||
async getPropertyList(): Promise<{ name: string, value: JSHandleChannel }[]> {
|
||||
const map = await this._object.getProperties();
|
||||
const result = [];
|
||||
|
|
|
|||
|
|
@ -84,13 +84,12 @@ export class RouteDispatcher extends Dispatcher<Route, RouteInitializer> impleme
|
|||
await this._object.continue(params.overrides);
|
||||
}
|
||||
|
||||
async fulfill(params: { response: { status?: number, headers?: types.Headers, contentType?: string, body: Binary } }): Promise<void> {
|
||||
async fulfill(params: { response: { status?: number, headers?: types.Headers, contentType?: string, body: string, isBase64: boolean } }): Promise<void> {
|
||||
const { response } = params;
|
||||
await this._object.fulfill({
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
contentType: response.contentType,
|
||||
body: Buffer.from(response.body, 'base64'),
|
||||
body: response.isBase64 ? Buffer.from(response.body, 'base64') : response.body,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,10 +33,10 @@ export class Transport {
|
|||
this.onclose = undefined;
|
||||
}
|
||||
|
||||
send(message: any) {
|
||||
send(message: string) {
|
||||
if (this._closed)
|
||||
throw new Error('Pipe has been closed');
|
||||
const data = Buffer.from(JSON.stringify(message), 'utf-8');
|
||||
const data = Buffer.from(message, 'utf-8');
|
||||
const dataLength = Buffer.alloc(4);
|
||||
dataLength.writeUInt32LE(data.length, 0);
|
||||
this._pipeWrite.write(dataLength);
|
||||
|
|
@ -70,7 +70,7 @@ export class Transport {
|
|||
this._bytesLeft = 0;
|
||||
this._waitForNextTask(() => {
|
||||
if (this.onmessage)
|
||||
this.onmessage.call(null, JSON.parse(message.toString('utf-8')));
|
||||
this.onmessage(message.toString('utf-8'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,8 +199,8 @@ export type FulfillResponse = {
|
|||
export type NormalizedFulfillResponse = {
|
||||
status: number,
|
||||
headers: Headers,
|
||||
contentType?: string,
|
||||
body: string | Buffer,
|
||||
body: string,
|
||||
isBase64: boolean,
|
||||
};
|
||||
|
||||
export type NetworkCookie = {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
*/
|
||||
|
||||
import * as frames from '../frames';
|
||||
import { assert, helper } from '../helper';
|
||||
import { assert } from '../helper';
|
||||
import * as network from '../network';
|
||||
import * as types from '../types';
|
||||
import { Protocol } from './protocol';
|
||||
|
|
@ -69,34 +69,20 @@ export class WKInterceptableRequest implements network.RouteDelegate {
|
|||
async fulfill(response: types.NormalizedFulfillResponse) {
|
||||
await this._interceptedPromise;
|
||||
|
||||
const base64Encoded = !!response.body && !helper.isString(response.body);
|
||||
const responseBody = response.body ? (base64Encoded ? response.body.toString('base64') : response.body as string) : '';
|
||||
|
||||
const responseHeaders: { [s: string]: string; } = {};
|
||||
for (const header of Object.keys(response.headers))
|
||||
responseHeaders[header.toLowerCase()] = String(response.headers[header]);
|
||||
let mimeType = base64Encoded ? 'application/octet-stream' : 'text/plain';
|
||||
if (response.contentType) {
|
||||
responseHeaders['content-type'] = response.contentType;
|
||||
const index = response.contentType.indexOf(';');
|
||||
if (index !== -1)
|
||||
mimeType = response.contentType.substring(0, index).trimEnd();
|
||||
else
|
||||
mimeType = response.contentType.trim();
|
||||
}
|
||||
if (responseBody && !('content-length' in responseHeaders))
|
||||
responseHeaders['content-length'] = String(Buffer.byteLength(responseBody));
|
||||
|
||||
// In certain cases, protocol will return error if the request was already canceled
|
||||
// or the page was closed. We should tolerate these errors.
|
||||
let mimeType = response.isBase64 ? 'application/octet-stream' : 'text/plain';
|
||||
const contentType = response.headers['content-type'];
|
||||
if (contentType)
|
||||
mimeType = contentType.split(';')[0].trim();
|
||||
await this._session.sendMayFail('Network.interceptRequestWithResponse', {
|
||||
requestId: this._requestId,
|
||||
status: response.status,
|
||||
statusText: network.STATUS_TEXTS[String(response.status)],
|
||||
mimeType,
|
||||
headers: responseHeaders,
|
||||
base64Encoded,
|
||||
content: responseBody
|
||||
headers: response.headers,
|
||||
base64Encoded: response.isBase64,
|
||||
content: response.body
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue