mirror of
https://github.com/gorhill/uBlock.git
synced 2024-10-06 09:37:12 +02:00
[firefox] Fix DOM inspector being broken in private window
Related issue:
https://github.com/uBlockOrigin/uBlock-issues/issues/3004
Related commit:
ee83a4304a
Use extensions API message ports to establish direct communication
between content script and logger window.
This commit is contained in:
parent
fddca0b7cb
commit
15b1250c99
@ -25,22 +25,19 @@
|
||||
/******************************************************************************/
|
||||
|
||||
const svgRoot = document.querySelector('svg');
|
||||
let inspectorContentPort;
|
||||
|
||||
const quit = ( ) => {
|
||||
inspectorContentPort.postMessage({ what: 'quitInspector' });
|
||||
const shutdown = ( ) => {
|
||||
inspectorContentPort.close();
|
||||
inspectorContentPort.onmessage = inspectorContentPort.onmessageerror = null;
|
||||
inspectorContentPort = undefined;
|
||||
loggerPort.postMessage({ what: 'quitInspector' });
|
||||
loggerPort.close();
|
||||
loggerPort.onmessage = loggerPort.onmessageerror = null;
|
||||
loggerPort = undefined;
|
||||
};
|
||||
|
||||
const onMessage = (msg, fromLogger) => {
|
||||
const contentInspectorChannel = ev => {
|
||||
const msg = ev.data || {};
|
||||
switch ( msg.what ) {
|
||||
case 'quitInspector': {
|
||||
quit();
|
||||
shutdown();
|
||||
break;
|
||||
}
|
||||
case 'svgPaths': {
|
||||
@ -52,41 +49,20 @@ const onMessage = (msg, fromLogger) => {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if ( typeof fromLogger !== 'boolean' ) { return; }
|
||||
if ( fromLogger ) {
|
||||
inspectorContentPort.postMessage(msg);
|
||||
} else {
|
||||
loggerPort.postMessage(msg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Wait for the content script to establish communication
|
||||
|
||||
let inspectorContentPort;
|
||||
|
||||
let loggerPort = new globalThis.BroadcastChannel('loggerInspector');
|
||||
loggerPort.onmessage = ev => {
|
||||
const msg = ev.data || {};
|
||||
onMessage(msg, true);
|
||||
};
|
||||
loggerPort.onmessageerror = ( ) => {
|
||||
quit();
|
||||
};
|
||||
|
||||
globalThis.addEventListener('message', ev => {
|
||||
const msg = ev.data || {};
|
||||
if ( msg.what !== 'startInspector' ) { return; }
|
||||
if ( Array.isArray(ev.ports) === false ) { return; }
|
||||
if ( ev.ports.length === 0 ) { return; }
|
||||
inspectorContentPort = ev.ports[0];
|
||||
inspectorContentPort.onmessage = ev => {
|
||||
const msg = ev.data || {};
|
||||
onMessage(msg, false);
|
||||
};
|
||||
inspectorContentPort.onmessageerror = ( ) => {
|
||||
quit();
|
||||
};
|
||||
inspectorContentPort.onmessage = contentInspectorChannel;
|
||||
inspectorContentPort.onmessageerror = shutdown;
|
||||
inspectorContentPort.postMessage({ what: 'startInspector' });
|
||||
}, { once: true });
|
||||
|
||||
/******************************************************************************/
|
||||
|
@ -42,21 +42,97 @@ let inspectedURL = '';
|
||||
let inspectedHostname = '';
|
||||
let uidGenerator = 1;
|
||||
|
||||
/******************************************************************************/
|
||||
/*******************************************************************************
|
||||
*
|
||||
* How it works:
|
||||
*
|
||||
* 1. The logger/inspector is enabled from the logger window
|
||||
*
|
||||
* 2. The inspector content script is injected in the root frame of the tab
|
||||
* currently selected in the logger
|
||||
*
|
||||
* 3. The inspector content script asks the logger/inspector to establish
|
||||
* a two-way communication channel
|
||||
*
|
||||
* 3. The inspector content script embed an inspector frame in the document
|
||||
* being inspected and waits for the inspector frame to be fully loaded
|
||||
*
|
||||
* 4. The inspector content script sends a messaging port object to the
|
||||
* embedded inspector frame for a two-way communication channel between
|
||||
* the inspector frame and the inspector content script
|
||||
*
|
||||
* 5. The inspector content script sends dom information to the
|
||||
* logger/inspector
|
||||
*
|
||||
* */
|
||||
|
||||
const inspectorFramePort = new globalThis.BroadcastChannel('loggerInspector');
|
||||
inspectorFramePort.onmessage = ev => {
|
||||
const msg = ev.data || {};
|
||||
if ( msg.what === 'domLayoutFull' ) {
|
||||
inspectedURL = msg.url;
|
||||
inspectedHostname = msg.hostname;
|
||||
renderDOMFull(msg);
|
||||
} else if ( msg.what === 'domLayoutIncremental' ) {
|
||||
renderDOMIncremental(msg);
|
||||
}
|
||||
};
|
||||
inspectorFramePort.onmessageerror = ( ) => {
|
||||
};
|
||||
const contentInspectorChannel = (( ) => {
|
||||
let bcChannel;
|
||||
let toContentPort;
|
||||
|
||||
const start = ( ) => {
|
||||
bcChannel = new globalThis.BroadcastChannel('contentInspectorChannel');
|
||||
bcChannel.onmessage = ev => {
|
||||
const msg = ev.data || {};
|
||||
connect(msg.tabId, msg.frameId);
|
||||
};
|
||||
browser.webNavigation.onDOMContentLoaded.addListener(onContentLoaded);
|
||||
};
|
||||
|
||||
const shutdown = ( ) => {
|
||||
browser.webNavigation.onDOMContentLoaded.removeListener(onContentLoaded);
|
||||
disconnect();
|
||||
bcChannel.close();
|
||||
bcChannel.onmessage = null;
|
||||
bcChannel = undefined;
|
||||
};
|
||||
|
||||
const connect = (tabId, frameId) => {
|
||||
disconnect();
|
||||
try {
|
||||
toContentPort = browser.tabs.connect(tabId, { frameId });
|
||||
toContentPort.onMessage.addListener(onContentMessage);
|
||||
toContentPort.onDisconnect.addListener(onContentDisconnect);
|
||||
} catch(_) {
|
||||
}
|
||||
};
|
||||
|
||||
const disconnect = ( ) => {
|
||||
if ( toContentPort === undefined ) { return; }
|
||||
toContentPort.onMessage.removeListener(onContentMessage);
|
||||
toContentPort.onDisconnect.removeListener(onContentDisconnect);
|
||||
toContentPort.disconnect();
|
||||
toContentPort = undefined;
|
||||
};
|
||||
|
||||
const send = msg => {
|
||||
if ( toContentPort === undefined ) { return; }
|
||||
toContentPort.postMessage(msg);
|
||||
};
|
||||
|
||||
const onContentMessage = msg => {
|
||||
if ( msg.what === 'domLayoutFull' ) {
|
||||
inspectedURL = msg.url;
|
||||
inspectedHostname = msg.hostname;
|
||||
renderDOMFull(msg);
|
||||
} else if ( msg.what === 'domLayoutIncremental' ) {
|
||||
renderDOMIncremental(msg);
|
||||
}
|
||||
};
|
||||
|
||||
const onContentDisconnect = ( ) => {
|
||||
disconnect();
|
||||
};
|
||||
|
||||
const onContentLoaded = details => {
|
||||
if ( details.tabId !== inspectedTabId ) { return; }
|
||||
if ( details.frameId !== 0 ) { return; }
|
||||
disconnect();
|
||||
injectInspector();
|
||||
};
|
||||
|
||||
return { start, disconnect, send, shutdown };
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
@ -345,7 +421,7 @@ const startDialog = (( ) => {
|
||||
};
|
||||
|
||||
const showCommitted = function() {
|
||||
inspectorFramePort.postMessage({
|
||||
contentInspectorChannel.send({
|
||||
what: 'showCommitted',
|
||||
hide: hideSelectors.join(',\n'),
|
||||
unhide: unhideSelectors.join(',\n')
|
||||
@ -353,7 +429,7 @@ const startDialog = (( ) => {
|
||||
};
|
||||
|
||||
const showInteractive = function() {
|
||||
inspectorFramePort.postMessage({
|
||||
contentInspectorChannel.send({
|
||||
what: 'showInteractive',
|
||||
hide: hideSelectors.join(',\n'),
|
||||
unhide: unhideSelectors.join(',\n')
|
||||
@ -432,7 +508,7 @@ const onClicked = ev => {
|
||||
|
||||
// Toggle cosmetic filter
|
||||
if ( dom.cl.has(target, 'filter') ) {
|
||||
inspectorFramePort.postMessage({
|
||||
contentInspectorChannel.send({
|
||||
what: 'toggleFilter',
|
||||
original: false,
|
||||
target: dom.cl.toggle(target, 'off'),
|
||||
@ -448,7 +524,7 @@ const onClicked = ev => {
|
||||
}
|
||||
// Toggle node
|
||||
else {
|
||||
inspectorFramePort.postMessage({
|
||||
contentInspectorChannel.send({
|
||||
what: 'toggleNodes',
|
||||
original: true,
|
||||
target: dom.cl.toggle(target, 'off') === false,
|
||||
@ -468,7 +544,7 @@ const onMouseOver = (( ) => {
|
||||
let mouseoverTarget = null;
|
||||
|
||||
const mouseoverTimer = vAPI.defer.create(( ) => {
|
||||
inspectorFramePort.postMessage({
|
||||
contentInspectorChannel.send({
|
||||
what: 'highlightOne',
|
||||
selector: selectorFromNode(mouseoverTarget),
|
||||
nid: nidFromNode(mouseoverTarget),
|
||||
@ -517,7 +593,7 @@ const injectInspector = (( ) => {
|
||||
/******************************************************************************/
|
||||
|
||||
const shutdownInspector = ( ) => {
|
||||
inspectorFramePort.postMessage({ what: 'quitInspector' });
|
||||
contentInspectorChannel.disconnect();
|
||||
logger.removeAllChildren(domTree);
|
||||
dom.cl.remove(inspector, 'vExpanded');
|
||||
inspectedTabId = 0;
|
||||
@ -537,14 +613,6 @@ const onTabIdChanged = ( ) => {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onDOMContentLoaded = details => {
|
||||
if ( details.tabId !== inspectedTabId ) { return; }
|
||||
if ( details.frameId !== 0 ) { return; }
|
||||
injectInspector();
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const toggleVCompactView = ( ) => {
|
||||
const state = dom.cl.toggle(inspector, 'vExpanded');
|
||||
const branches = qsa$('#domInspector li.branch');
|
||||
@ -561,7 +629,7 @@ const toggleHCompactView = ( ) => {
|
||||
|
||||
const revert = ( ) => {
|
||||
dom.cl.remove('#domTree .off', 'off');
|
||||
inspectorFramePort.postMessage({ what: 'resetToggledNodes' });
|
||||
contentInspectorChannel.send({ what: 'resetToggledNodes' });
|
||||
dom.cl.add(qs$(inspector, '.permatoolbar .revert'), 'disabled');
|
||||
dom.cl.add(qs$(inspector, '.permatoolbar .commit'), 'disabled');
|
||||
};
|
||||
@ -578,7 +646,7 @@ const toggleOn = ( ) => {
|
||||
dom.on('#domInspector .hCompactToggler', 'click', toggleHCompactView);
|
||||
dom.on('#domInspector .permatoolbar .revert', 'click', revert);
|
||||
dom.on('#domInspector .permatoolbar .commit', 'click', startDialog);
|
||||
browser.webNavigation.onDOMContentLoaded.addListener(onDOMContentLoaded);
|
||||
contentInspectorChannel.start();
|
||||
injectInspector();
|
||||
};
|
||||
|
||||
@ -596,7 +664,7 @@ const toggleOff = ( ) => {
|
||||
dom.off('#domInspector .hCompactToggler', 'click', toggleHCompactView);
|
||||
dom.off('#domInspector .permatoolbar .revert', 'click', revert);
|
||||
dom.off('#domInspector .permatoolbar .commit', 'click', startDialog);
|
||||
browser.webNavigation.onDOMContentLoaded.removeListener(onDOMContentLoaded);
|
||||
contentInspectorChannel.shutdown();
|
||||
inspectedTabId = 0;
|
||||
};
|
||||
|
||||
|
@ -1860,6 +1860,12 @@ const onMessage = (request, sender, callback) => {
|
||||
let response;
|
||||
switch ( request.what ) {
|
||||
case 'getInspectorArgs':
|
||||
const bc = new globalThis.BroadcastChannel('contentInspectorChannel');
|
||||
bc.postMessage({
|
||||
what: 'contentInspectorChannel',
|
||||
tabId: sender.tabId || 0,
|
||||
frameId: sender.frameId || 0,
|
||||
});
|
||||
response = {
|
||||
inspectorURL: vAPI.getURL(
|
||||
`/web_accessible_resources/dom-inspector.html?secret=${vAPI.warSecret.short()}`
|
||||
|
@ -19,6 +19,8 @@
|
||||
Home: https://github.com/gorhill/uBlock
|
||||
*/
|
||||
|
||||
/* globals browser */
|
||||
|
||||
'use strict';
|
||||
|
||||
/******************************************************************************/
|
||||
@ -35,9 +37,6 @@ if ( document.querySelector(`iframe[${vAPI.sessionId}]`) !== null ) { return; }
|
||||
/******************************************************************************/
|
||||
/******************************************************************************/
|
||||
|
||||
// Highlighter-related
|
||||
let inspectorRoot = null;
|
||||
|
||||
const nodeToIdMap = new WeakMap(); // No need to iterate
|
||||
|
||||
let blueNodes = [];
|
||||
@ -130,7 +129,7 @@ const domLayout = (( ) => {
|
||||
const localName = node.localName;
|
||||
if ( skipTagNames.has(localName) ) { return null; }
|
||||
// skip uBlock's own nodes
|
||||
if ( node === inspectorRoot ) { return null; }
|
||||
if ( node === inspectorFrame ) { return null; }
|
||||
if ( level === 0 && localName === 'body' ) {
|
||||
return new DomRoot();
|
||||
}
|
||||
@ -298,7 +297,7 @@ const domLayout = (( ) => {
|
||||
|
||||
if ( journalEntries.length === 0 ) { return; }
|
||||
|
||||
inspectorFramePort.postMessage({
|
||||
contentInspectorChannel.toLogger({
|
||||
what: 'domLayoutIncremental',
|
||||
url: window.location.href,
|
||||
hostname: window.location.hostname,
|
||||
@ -483,7 +482,7 @@ const highlightElements = ( ) => {
|
||||
|
||||
const path = [];
|
||||
for ( const elem of rwRedNodes.keys() ) {
|
||||
if ( elem === inspectorRoot ) { continue; }
|
||||
if ( elem === inspectorFrame ) { continue; }
|
||||
if ( rwGreenNodes.has(elem) ) { continue; }
|
||||
if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; }
|
||||
const rect = elem.getBoundingClientRect();
|
||||
@ -521,7 +520,7 @@ const highlightElements = ( ) => {
|
||||
|
||||
path.length = 0;
|
||||
for ( const elem of roRedNodes.keys() ) {
|
||||
if ( elem === inspectorRoot ) { continue; }
|
||||
if ( elem === inspectorFrame ) { continue; }
|
||||
if ( rwGreenNodes.has(elem) ) { continue; }
|
||||
if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; }
|
||||
const rect = elem.getBoundingClientRect();
|
||||
@ -541,7 +540,7 @@ const highlightElements = ( ) => {
|
||||
|
||||
path.length = 0;
|
||||
for ( const elem of blueNodes ) {
|
||||
if ( elem === inspectorRoot ) { continue; }
|
||||
if ( elem === inspectorFrame ) { continue; }
|
||||
if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; }
|
||||
const rect = elem.getBoundingClientRect();
|
||||
const xl = rect.left;
|
||||
@ -558,7 +557,7 @@ const highlightElements = ( ) => {
|
||||
}
|
||||
paths.push(path.join('') || 'M0 0');
|
||||
|
||||
inspectorFramePort.postMessage({
|
||||
contentInspectorChannel.toFrame({
|
||||
what: 'svgPaths',
|
||||
paths,
|
||||
});
|
||||
@ -637,7 +636,7 @@ const resetToggledNodes = ( ) => {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const start = ( ) => {
|
||||
const startInspector = ( ) => {
|
||||
const onReady = ( ) => {
|
||||
window.addEventListener('scroll', onScrolled, {
|
||||
capture: true,
|
||||
@ -647,7 +646,7 @@ const start = ( ) => {
|
||||
capture: true,
|
||||
passive: true,
|
||||
});
|
||||
inspectorFramePort.postMessage(domLayout.get());
|
||||
contentInspectorChannel.toLogger(domLayout.get());
|
||||
vAPI.domFilterer.toggle(false, highlightElements);
|
||||
};
|
||||
if ( document.readyState === 'loading' ) {
|
||||
@ -659,7 +658,7 @@ const start = ( ) => {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const shutdown = ( ) => {
|
||||
const shutdownInspector = ( ) => {
|
||||
cosmeticFilterMapper.shutdown();
|
||||
domLayout.shutdown();
|
||||
window.removeEventListener('scroll', onScrolled, {
|
||||
@ -670,13 +669,12 @@ const shutdown = ( ) => {
|
||||
capture: true,
|
||||
passive: true,
|
||||
});
|
||||
inspectorFramePort.close();
|
||||
inspectorFramePort = undefined;
|
||||
contentInspectorChannel.shutdown();
|
||||
vAPI.userStylesheet.remove(inspectorCSS);
|
||||
vAPI.userStylesheet.apply();
|
||||
if ( inspectorRoot === null ) { return; }
|
||||
inspectorRoot.remove();
|
||||
inspectorRoot = null;
|
||||
if ( inspectorFrame === null ) { return; }
|
||||
inspectorFrame.remove();
|
||||
inspectorFrame = null;
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
@ -685,11 +683,11 @@ const shutdown = ( ) => {
|
||||
const onMessage = request => {
|
||||
switch ( request.what ) {
|
||||
case 'startInspector':
|
||||
start();
|
||||
startInspector();
|
||||
break;
|
||||
|
||||
case 'quitInspector':
|
||||
shutdown();
|
||||
shutdownInspector();
|
||||
break;
|
||||
|
||||
case 'commitFilters':
|
||||
@ -756,15 +754,97 @@ const onMessage = request => {
|
||||
}
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
/*******************************************************************************
|
||||
*
|
||||
* Establish two-way communication with logger/inspector window and
|
||||
* inspector frame
|
||||
*
|
||||
* */
|
||||
|
||||
const contentInspectorChannel = (( ) => {
|
||||
let toLoggerPort;
|
||||
let toFramePort;
|
||||
|
||||
const toLogger = msg => {
|
||||
if ( toLoggerPort === undefined ) { return; }
|
||||
try {
|
||||
toLoggerPort.postMessage(msg);
|
||||
} catch(_) {
|
||||
shutdownInspector();
|
||||
}
|
||||
};
|
||||
|
||||
const onLoggerMessage = msg => {
|
||||
onMessage(msg);
|
||||
};
|
||||
|
||||
const onLoggerDisconnect = ( ) => {
|
||||
shutdownInspector();
|
||||
};
|
||||
|
||||
const onLoggerConnect = port => {
|
||||
browser.runtime.onConnect.removeListener(onLoggerConnect);
|
||||
toLoggerPort = port;
|
||||
port.onMessage.addListener(onLoggerMessage);
|
||||
port.onDisconnect.addListener(onLoggerDisconnect);
|
||||
};
|
||||
|
||||
const toFrame = msg => {
|
||||
if ( toFramePort === undefined ) { return; }
|
||||
toFramePort.postMessage(msg);
|
||||
};
|
||||
|
||||
const shutdown = ( ) => {
|
||||
if ( toFramePort !== undefined ) {
|
||||
toFrame({ what: 'quitInspector' });
|
||||
toFramePort.onmessage = null;
|
||||
toFramePort.close();
|
||||
toFramePort = undefined;
|
||||
}
|
||||
if ( toLoggerPort !== undefined ) {
|
||||
toLoggerPort.onMessage.removeListener(onLoggerMessage);
|
||||
toLoggerPort.onDisconnect.removeListener(onLoggerDisconnect);
|
||||
toLoggerPort.disconnect();
|
||||
toLoggerPort = undefined;
|
||||
}
|
||||
browser.runtime.onConnect.removeListener(onLoggerConnect);
|
||||
};
|
||||
|
||||
const start = async ( ) => {
|
||||
browser.runtime.onConnect.addListener(onLoggerConnect);
|
||||
const inspectorArgs = await vAPI.messaging.send('domInspectorContent', {
|
||||
what: 'getInspectorArgs',
|
||||
});
|
||||
if ( typeof inspectorArgs !== 'object' ) { return; }
|
||||
if ( inspectorArgs === null ) { return; }
|
||||
return new Promise(resolve => {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.setAttribute(vAPI.sessionId, '');
|
||||
document.documentElement.append(iframe);
|
||||
iframe.addEventListener('load', ( ) => {
|
||||
iframe.setAttribute(`${vAPI.sessionId}-loaded`, '');
|
||||
const channel = new MessageChannel();
|
||||
toFramePort = channel.port1;
|
||||
toFramePort.onmessage = ev => {
|
||||
const msg = ev.data || {};
|
||||
if ( msg.what !== 'startInspector' ) { return; }
|
||||
resolve(iframe);
|
||||
};
|
||||
iframe.contentWindow.postMessage(
|
||||
{ what: 'startInspector' },
|
||||
inspectorArgs.inspectorURL,
|
||||
[ channel.port2 ]
|
||||
);
|
||||
}, { once: true });
|
||||
iframe.contentWindow.location = inspectorArgs.inspectorURL;
|
||||
});
|
||||
};
|
||||
|
||||
return { start, toLogger, toFrame, shutdown };
|
||||
})();
|
||||
|
||||
|
||||
// Install DOM inspector widget
|
||||
let inspectorArgs = await vAPI.messaging.send('domInspectorContent', {
|
||||
what: 'getInspectorArgs',
|
||||
});
|
||||
if ( typeof inspectorArgs !== 'object' ) { return; }
|
||||
if ( inspectorArgs === null ) { return; }
|
||||
|
||||
const inspectorCSSStyle = [
|
||||
'background: transparent',
|
||||
'border: 0',
|
||||
@ -799,31 +879,12 @@ const inspectorCSS = `
|
||||
vAPI.userStylesheet.add(inspectorCSS);
|
||||
vAPI.userStylesheet.apply();
|
||||
|
||||
inspectorRoot = document.createElement('iframe');
|
||||
inspectorRoot.setAttribute(vAPI.sessionId, '');
|
||||
document.documentElement.append(inspectorRoot);
|
||||
let inspectorFrame = await contentInspectorChannel.start();
|
||||
if ( inspectorFrame instanceof HTMLIFrameElement === false ) {
|
||||
return shutdownInspector();
|
||||
}
|
||||
|
||||
let inspectorFramePort;
|
||||
|
||||
inspectorRoot.addEventListener('load', ( ) => {
|
||||
const channel = new MessageChannel();
|
||||
inspectorFramePort = channel.port1;
|
||||
inspectorFramePort.onmessage = ev => {
|
||||
const msg = ev.data || {};
|
||||
onMessage(msg);
|
||||
};
|
||||
inspectorFramePort.onmessageerror = ( ) => {
|
||||
shutdown();
|
||||
};
|
||||
inspectorRoot.setAttribute(`${vAPI.sessionId}-loaded`, '');
|
||||
inspectorRoot.contentWindow.postMessage(
|
||||
{ what: 'startInspector' },
|
||||
inspectorArgs.inspectorURL,
|
||||
[ channel.port2 ]
|
||||
);
|
||||
}, { once: true });
|
||||
|
||||
inspectorRoot.contentWindow.location = inspectorArgs.inspectorURL;
|
||||
startInspector();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user