browser(firefox): manage scripts to evaluate on load on front-end (#12101)
This commit is contained in:
parent
e9a135406c
commit
618cc66c8d
|
|
@ -1,2 +1,2 @@
|
||||||
1316
|
1317
|
||||||
Changed: aslushnikov@gmail.com Wed Jan 26 17:24:09 MST 2022
|
Changed: pavel.feldman@gmail.com Mon 14 Feb 2022 03:52:34 PM PST
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ class TargetRegistry {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scriptsToEvaluateOnNewDocument: target.browserContext().scriptsToEvaluateOnNewDocument,
|
initScripts: target.browserContext().initScripts,
|
||||||
bindings: target.browserContext().bindings,
|
bindings: target.browserContext().bindings,
|
||||||
settings: target.browserContext().settings,
|
settings: target.browserContext().settings,
|
||||||
};
|
};
|
||||||
|
|
@ -361,6 +361,7 @@ class PageTarget {
|
||||||
this._screencastRecordingInfo = undefined;
|
this._screencastRecordingInfo = undefined;
|
||||||
this._dialogs = new Map();
|
this._dialogs = new Map();
|
||||||
this.forcedColors = 'no-override';
|
this.forcedColors = 'no-override';
|
||||||
|
this._pageInitScripts = [];
|
||||||
|
|
||||||
const navigationListener = {
|
const navigationListener = {
|
||||||
QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]),
|
QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]),
|
||||||
|
|
@ -523,8 +524,13 @@ class PageTarget {
|
||||||
await this._channel.connect('').send('ensurePermissions', {}).catch(e => void e);
|
await this._channel.connect('').send('ensurePermissions', {}).catch(e => void e);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addScriptToEvaluateOnNewDocument(script) {
|
async setInitScripts(scripts) {
|
||||||
await this._channel.connect('').send('addScriptToEvaluateOnNewDocument', script).catch(e => void e);
|
this._pageInitScripts = scripts;
|
||||||
|
await this.pushInitScripts();
|
||||||
|
}
|
||||||
|
|
||||||
|
async pushInitScripts() {
|
||||||
|
await this._channel.connect('').send('setInitScripts', [...this._browserContext.initScripts, ...this._pageInitScripts]).catch(e => void e);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addBinding(worldName, name, script) {
|
async addBinding(worldName, name, script) {
|
||||||
|
|
@ -708,7 +714,7 @@ class BrowserContext {
|
||||||
this.forcedColors = 'no-override';
|
this.forcedColors = 'no-override';
|
||||||
this.reducedMotion = 'none';
|
this.reducedMotion = 'none';
|
||||||
this.videoRecordingOptions = undefined;
|
this.videoRecordingOptions = undefined;
|
||||||
this.scriptsToEvaluateOnNewDocument = [];
|
this.initScripts = [];
|
||||||
this.bindings = [];
|
this.bindings = [];
|
||||||
this.settings = {};
|
this.settings = {};
|
||||||
this.pages = new Set();
|
this.pages = new Set();
|
||||||
|
|
@ -804,9 +810,9 @@ class BrowserContext {
|
||||||
await Promise.all(Array.from(this.pages).map(page => page.updateViewportSize()));
|
await Promise.all(Array.from(this.pages).map(page => page.updateViewportSize()));
|
||||||
}
|
}
|
||||||
|
|
||||||
async addScriptToEvaluateOnNewDocument(script) {
|
async setInitScripts(scripts) {
|
||||||
this.scriptsToEvaluateOnNewDocument.push(script);
|
this.initScripts = scripts;
|
||||||
await Promise.all(Array.from(this.pages).map(page => page.addScriptToEvaluateOnNewDocument(script)));
|
await Promise.all(Array.from(this.pages).map(page => page.pushInitScripts()));
|
||||||
}
|
}
|
||||||
|
|
||||||
async addBinding(worldName, name, script) {
|
async addBinding(worldName, name, script) {
|
||||||
|
|
|
||||||
|
|
@ -73,15 +73,20 @@ class FrameTree {
|
||||||
return this._runtime;
|
return this._runtime;
|
||||||
}
|
}
|
||||||
|
|
||||||
addScriptToEvaluateOnNewDocument(script, worldName) {
|
setInitScripts(scripts) {
|
||||||
worldName = worldName || '';
|
for (const world of this._isolatedWorlds.values())
|
||||||
const existing = this._isolatedWorlds.has(worldName);
|
world._scriptsToEvaluateOnNewDocument = [];
|
||||||
const world = this._ensureWorld(worldName);
|
|
||||||
world._scriptsToEvaluateOnNewDocument.push(script);
|
for (let { worldName, script } of scripts) {
|
||||||
// FIXME: 'should inherit http credentials from browser context' fails without this
|
worldName = worldName || '';
|
||||||
if (worldName && !existing) {
|
const existing = this._isolatedWorlds.has(worldName);
|
||||||
for (const frame of this.frames())
|
const world = this._ensureWorld(worldName);
|
||||||
frame._createIsolatedContext(worldName);
|
world._scriptsToEvaluateOnNewDocument.push(script);
|
||||||
|
// FIXME: 'should inherit http credentials from browser context' fails without this
|
||||||
|
if (worldName && !existing) {
|
||||||
|
for (const frame of this.frames())
|
||||||
|
frame._createIsolatedContext(worldName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,6 @@ class PageAgent {
|
||||||
this._runtime.events.onBindingCalled(this._onBindingCalled.bind(this)),
|
this._runtime.events.onBindingCalled(this._onBindingCalled.bind(this)),
|
||||||
browserChannel.register('page', {
|
browserChannel.register('page', {
|
||||||
addBinding: ({ worldName, name, script }) => this._frameTree.addBinding(worldName, name, script),
|
addBinding: ({ worldName, name, script }) => this._frameTree.addBinding(worldName, name, script),
|
||||||
addScriptToEvaluateOnNewDocument: ({script, worldName}) => this._frameTree.addScriptToEvaluateOnNewDocument(script, worldName),
|
|
||||||
adoptNode: this._adoptNode.bind(this),
|
adoptNode: this._adoptNode.bind(this),
|
||||||
crash: this._crash.bind(this),
|
crash: this._crash.bind(this),
|
||||||
describeNode: this._describeNode.bind(this),
|
describeNode: this._describeNode.bind(this),
|
||||||
|
|
|
||||||
|
|
@ -84,11 +84,10 @@ function initialize() {
|
||||||
if (!response)
|
if (!response)
|
||||||
return;
|
return;
|
||||||
const {
|
const {
|
||||||
scriptsToEvaluateOnNewDocument = [],
|
initScripts = [],
|
||||||
bindings = [],
|
bindings = [],
|
||||||
settings = {}
|
settings = {}
|
||||||
} = response || {};
|
} = response || {};
|
||||||
|
|
||||||
// Enforce focused state for all top level documents.
|
// Enforce focused state for all top level documents.
|
||||||
docShell.overrideHasFocus = true;
|
docShell.overrideHasFocus = true;
|
||||||
docShell.forceActiveState = true;
|
docShell.forceActiveState = true;
|
||||||
|
|
@ -99,14 +98,13 @@ function initialize() {
|
||||||
}
|
}
|
||||||
for (const { worldName, name, script } of bindings)
|
for (const { worldName, name, script } of bindings)
|
||||||
frameTree.addBinding(worldName, name, script);
|
frameTree.addBinding(worldName, name, script);
|
||||||
for (const script of scriptsToEvaluateOnNewDocument)
|
frameTree.setInitScripts(initScripts);
|
||||||
frameTree.addScriptToEvaluateOnNewDocument(script);
|
|
||||||
|
|
||||||
pageAgent = new PageAgent(messageManager, channel, frameTree);
|
pageAgent = new PageAgent(messageManager, channel, frameTree);
|
||||||
|
|
||||||
channel.register('', {
|
channel.register('', {
|
||||||
addScriptToEvaluateOnNewDocument(script) {
|
setInitScripts(scripts) {
|
||||||
frameTree.addScriptToEvaluateOnNewDocument(script);
|
frameTree.setInitScripts(scripts);
|
||||||
},
|
},
|
||||||
|
|
||||||
addBinding({worldName, name, script}) {
|
addBinding({worldName, name, script}) {
|
||||||
|
|
|
||||||
|
|
@ -253,8 +253,8 @@ class BrowserHandler {
|
||||||
await this._targetRegistry.browserContextForId(browserContextId).applySetting('scrollbarsHidden', nullToUndefined(hidden));
|
await this._targetRegistry.browserContextForId(browserContextId).applySetting('scrollbarsHidden', nullToUndefined(hidden));
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Browser.addScriptToEvaluateOnNewDocument']({browserContextId, script}) {
|
async ['Browser.setInitScripts']({browserContextId, scripts}) {
|
||||||
await this._targetRegistry.browserContextForId(browserContextId).addScriptToEvaluateOnNewDocument(script);
|
await this._targetRegistry.browserContextForId(browserContextId).setInitScripts(scripts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Browser.addBinding']({browserContextId, worldName, name, script}) {
|
async ['Browser.addBinding']({browserContextId, worldName, name, script}) {
|
||||||
|
|
|
||||||
|
|
@ -334,8 +334,8 @@ class PageHandler {
|
||||||
return await this._contentPage.send('scrollIntoViewIfNeeded', options);
|
return await this._contentPage.send('scrollIntoViewIfNeeded', options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Page.addScriptToEvaluateOnNewDocument'](options) {
|
async ['Page.setInitScripts']({ scripts }) {
|
||||||
return await this._contentPage.send('addScriptToEvaluateOnNewDocument', options);
|
return await this._pageTarget.setInitScripts(scripts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Page.dispatchKeyEvent'](options) {
|
async ['Page.dispatchKeyEvent'](options) {
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,10 @@ pageTypes.Clip = {
|
||||||
height: t.Number,
|
height: t.Number,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pageTypes.InitScript = {
|
||||||
|
script: t.String,
|
||||||
|
worldName: t.Optional(t.String),
|
||||||
|
};
|
||||||
|
|
||||||
const runtimeTypes = {};
|
const runtimeTypes = {};
|
||||||
runtimeTypes.RemoteObject = {
|
runtimeTypes.RemoteObject = {
|
||||||
|
|
@ -381,10 +385,10 @@ const Browser = {
|
||||||
hidden: t.Boolean,
|
hidden: t.Boolean,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'addScriptToEvaluateOnNewDocument': {
|
'setInitScripts': {
|
||||||
params: {
|
params: {
|
||||||
browserContextId: t.Optional(t.String),
|
browserContextId: t.Optional(t.String),
|
||||||
script: t.String,
|
scripts: t.Array(pageTypes.InitScript),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'addBinding': {
|
'addBinding': {
|
||||||
|
|
@ -802,10 +806,9 @@ const Page = {
|
||||||
rect: t.Optional(pageTypes.Rect),
|
rect: t.Optional(pageTypes.Rect),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'addScriptToEvaluateOnNewDocument': {
|
'setInitScripts': {
|
||||||
params: {
|
params: {
|
||||||
script: t.String,
|
scripts: t.Array(pageTypes.InitScript)
|
||||||
worldName: t.Optional(t.String),
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'navigate': {
|
'navigate': {
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
1316
|
1317
|
||||||
Changed: aslushnikov@gmail.com Wed Jan 26 17:24:09 MST 2022
|
Changed: pavel.feldman@gmail.com Mon 14 Feb 2022 03:52:34 PM PST
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ class TargetRegistry {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scriptsToEvaluateOnNewDocument: target.browserContext().scriptsToEvaluateOnNewDocument,
|
initScripts: target.browserContext().initScripts,
|
||||||
bindings: target.browserContext().bindings,
|
bindings: target.browserContext().bindings,
|
||||||
settings: target.browserContext().settings,
|
settings: target.browserContext().settings,
|
||||||
};
|
};
|
||||||
|
|
@ -361,6 +361,7 @@ class PageTarget {
|
||||||
this._screencastRecordingInfo = undefined;
|
this._screencastRecordingInfo = undefined;
|
||||||
this._dialogs = new Map();
|
this._dialogs = new Map();
|
||||||
this.forcedColors = 'no-override';
|
this.forcedColors = 'no-override';
|
||||||
|
this._pageInitScripts = [];
|
||||||
|
|
||||||
const navigationListener = {
|
const navigationListener = {
|
||||||
QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]),
|
QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]),
|
||||||
|
|
@ -523,8 +524,13 @@ class PageTarget {
|
||||||
await this._channel.connect('').send('ensurePermissions', {}).catch(e => void e);
|
await this._channel.connect('').send('ensurePermissions', {}).catch(e => void e);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addScriptToEvaluateOnNewDocument(script) {
|
async setInitScripts(scripts) {
|
||||||
await this._channel.connect('').send('addScriptToEvaluateOnNewDocument', script).catch(e => void e);
|
this._pageInitScripts = scripts;
|
||||||
|
await this.pushInitScripts();
|
||||||
|
}
|
||||||
|
|
||||||
|
async pushInitScripts() {
|
||||||
|
await this._channel.connect('').send('setInitScripts', [...this._browserContext.initScripts, ...this._pageInitScripts]).catch(e => void e);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addBinding(worldName, name, script) {
|
async addBinding(worldName, name, script) {
|
||||||
|
|
@ -708,7 +714,7 @@ class BrowserContext {
|
||||||
this.forcedColors = 'no-override';
|
this.forcedColors = 'no-override';
|
||||||
this.reducedMotion = 'none';
|
this.reducedMotion = 'none';
|
||||||
this.videoRecordingOptions = undefined;
|
this.videoRecordingOptions = undefined;
|
||||||
this.scriptsToEvaluateOnNewDocument = [];
|
this.initScripts = [];
|
||||||
this.bindings = [];
|
this.bindings = [];
|
||||||
this.settings = {};
|
this.settings = {};
|
||||||
this.pages = new Set();
|
this.pages = new Set();
|
||||||
|
|
@ -804,9 +810,9 @@ class BrowserContext {
|
||||||
await Promise.all(Array.from(this.pages).map(page => page.updateViewportSize()));
|
await Promise.all(Array.from(this.pages).map(page => page.updateViewportSize()));
|
||||||
}
|
}
|
||||||
|
|
||||||
async addScriptToEvaluateOnNewDocument(script) {
|
async setInitScripts(scripts) {
|
||||||
this.scriptsToEvaluateOnNewDocument.push(script);
|
this.initScripts = scripts;
|
||||||
await Promise.all(Array.from(this.pages).map(page => page.addScriptToEvaluateOnNewDocument(script)));
|
await Promise.all(Array.from(this.pages).map(page => page.pushInitScripts()));
|
||||||
}
|
}
|
||||||
|
|
||||||
async addBinding(worldName, name, script) {
|
async addBinding(worldName, name, script) {
|
||||||
|
|
|
||||||
|
|
@ -73,15 +73,20 @@ class FrameTree {
|
||||||
return this._runtime;
|
return this._runtime;
|
||||||
}
|
}
|
||||||
|
|
||||||
addScriptToEvaluateOnNewDocument(script, worldName) {
|
setInitScripts(scripts) {
|
||||||
worldName = worldName || '';
|
for (const world of this._isolatedWorlds.values())
|
||||||
const existing = this._isolatedWorlds.has(worldName);
|
world._scriptsToEvaluateOnNewDocument = [];
|
||||||
const world = this._ensureWorld(worldName);
|
|
||||||
world._scriptsToEvaluateOnNewDocument.push(script);
|
for (let { worldName, script } of scripts) {
|
||||||
// FIXME: 'should inherit http credentials from browser context' fails without this
|
worldName = worldName || '';
|
||||||
if (worldName && !existing) {
|
const existing = this._isolatedWorlds.has(worldName);
|
||||||
for (const frame of this.frames())
|
const world = this._ensureWorld(worldName);
|
||||||
frame._createIsolatedContext(worldName);
|
world._scriptsToEvaluateOnNewDocument.push(script);
|
||||||
|
// FIXME: 'should inherit http credentials from browser context' fails without this
|
||||||
|
if (worldName && !existing) {
|
||||||
|
for (const frame of this.frames())
|
||||||
|
frame._createIsolatedContext(worldName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,6 @@ class PageAgent {
|
||||||
this._runtime.events.onBindingCalled(this._onBindingCalled.bind(this)),
|
this._runtime.events.onBindingCalled(this._onBindingCalled.bind(this)),
|
||||||
browserChannel.register('page', {
|
browserChannel.register('page', {
|
||||||
addBinding: ({ worldName, name, script }) => this._frameTree.addBinding(worldName, name, script),
|
addBinding: ({ worldName, name, script }) => this._frameTree.addBinding(worldName, name, script),
|
||||||
addScriptToEvaluateOnNewDocument: ({script, worldName}) => this._frameTree.addScriptToEvaluateOnNewDocument(script, worldName),
|
|
||||||
adoptNode: this._adoptNode.bind(this),
|
adoptNode: this._adoptNode.bind(this),
|
||||||
crash: this._crash.bind(this),
|
crash: this._crash.bind(this),
|
||||||
describeNode: this._describeNode.bind(this),
|
describeNode: this._describeNode.bind(this),
|
||||||
|
|
|
||||||
|
|
@ -84,11 +84,10 @@ function initialize() {
|
||||||
if (!response)
|
if (!response)
|
||||||
return;
|
return;
|
||||||
const {
|
const {
|
||||||
scriptsToEvaluateOnNewDocument = [],
|
initScripts = [],
|
||||||
bindings = [],
|
bindings = [],
|
||||||
settings = {}
|
settings = {}
|
||||||
} = response || {};
|
} = response || {};
|
||||||
|
|
||||||
// Enforce focused state for all top level documents.
|
// Enforce focused state for all top level documents.
|
||||||
docShell.overrideHasFocus = true;
|
docShell.overrideHasFocus = true;
|
||||||
docShell.forceActiveState = true;
|
docShell.forceActiveState = true;
|
||||||
|
|
@ -99,14 +98,13 @@ function initialize() {
|
||||||
}
|
}
|
||||||
for (const { worldName, name, script } of bindings)
|
for (const { worldName, name, script } of bindings)
|
||||||
frameTree.addBinding(worldName, name, script);
|
frameTree.addBinding(worldName, name, script);
|
||||||
for (const script of scriptsToEvaluateOnNewDocument)
|
frameTree.setInitScripts(initScripts);
|
||||||
frameTree.addScriptToEvaluateOnNewDocument(script);
|
|
||||||
|
|
||||||
pageAgent = new PageAgent(messageManager, channel, frameTree);
|
pageAgent = new PageAgent(messageManager, channel, frameTree);
|
||||||
|
|
||||||
channel.register('', {
|
channel.register('', {
|
||||||
addScriptToEvaluateOnNewDocument(script) {
|
setInitScripts(scripts) {
|
||||||
frameTree.addScriptToEvaluateOnNewDocument(script);
|
frameTree.setInitScripts(scripts);
|
||||||
},
|
},
|
||||||
|
|
||||||
addBinding({worldName, name, script}) {
|
addBinding({worldName, name, script}) {
|
||||||
|
|
|
||||||
|
|
@ -253,8 +253,8 @@ class BrowserHandler {
|
||||||
await this._targetRegistry.browserContextForId(browserContextId).applySetting('scrollbarsHidden', nullToUndefined(hidden));
|
await this._targetRegistry.browserContextForId(browserContextId).applySetting('scrollbarsHidden', nullToUndefined(hidden));
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Browser.addScriptToEvaluateOnNewDocument']({browserContextId, script}) {
|
async ['Browser.setInitScripts']({browserContextId, scripts}) {
|
||||||
await this._targetRegistry.browserContextForId(browserContextId).addScriptToEvaluateOnNewDocument(script);
|
await this._targetRegistry.browserContextForId(browserContextId).setInitScripts(scripts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Browser.addBinding']({browserContextId, worldName, name, script}) {
|
async ['Browser.addBinding']({browserContextId, worldName, name, script}) {
|
||||||
|
|
|
||||||
|
|
@ -334,8 +334,8 @@ class PageHandler {
|
||||||
return await this._contentPage.send('scrollIntoViewIfNeeded', options);
|
return await this._contentPage.send('scrollIntoViewIfNeeded', options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Page.addScriptToEvaluateOnNewDocument'](options) {
|
async ['Page.setInitScripts']({ scripts }) {
|
||||||
return await this._contentPage.send('addScriptToEvaluateOnNewDocument', options);
|
return await this._pageTarget.setInitScripts(scripts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Page.dispatchKeyEvent'](options) {
|
async ['Page.dispatchKeyEvent'](options) {
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,10 @@ pageTypes.Clip = {
|
||||||
height: t.Number,
|
height: t.Number,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pageTypes.InitScript = {
|
||||||
|
script: t.String,
|
||||||
|
worldName: t.Optional(t.String),
|
||||||
|
};
|
||||||
|
|
||||||
const runtimeTypes = {};
|
const runtimeTypes = {};
|
||||||
runtimeTypes.RemoteObject = {
|
runtimeTypes.RemoteObject = {
|
||||||
|
|
@ -381,10 +385,10 @@ const Browser = {
|
||||||
hidden: t.Boolean,
|
hidden: t.Boolean,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'addScriptToEvaluateOnNewDocument': {
|
'setInitScripts': {
|
||||||
params: {
|
params: {
|
||||||
browserContextId: t.Optional(t.String),
|
browserContextId: t.Optional(t.String),
|
||||||
script: t.String,
|
scripts: t.Array(pageTypes.InitScript),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'addBinding': {
|
'addBinding': {
|
||||||
|
|
@ -802,10 +806,9 @@ const Page = {
|
||||||
rect: t.Optional(pageTypes.Rect),
|
rect: t.Optional(pageTypes.Rect),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'addScriptToEvaluateOnNewDocument': {
|
'setInitScripts': {
|
||||||
params: {
|
params: {
|
||||||
script: t.String,
|
scripts: t.Array(pageTypes.InitScript)
|
||||||
worldName: t.Optional(t.String),
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'navigate': {
|
'navigate': {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue