mirror of
https://github.com/gorhill/uBlock.git
synced 2024-10-06 09:37:12 +02:00
Fine tune hostname uncloaking through CNAME-lookup
Related issue: - https://github.com/uBlockOrigin/uBlock-issues/issues/780 Related commit: - https://github.com/gorhill/uBlock/commit/3a564c199260 This adds two new advanced settings: - cnameIgnoreRootDocument - Default to `true` - Tells uBO to skip CNAME-lookup for root document. - cnameReplayFullURL - Default to `false` - Tells uBO whether to replay the whole URL or just the origin part of it. Replaying only the origin part is meant to lower undue breakage and improve performance by avoiding repeating the pattern-matching of the whole URL -- which pattern-matching was most likely already accomplished with the original request. This commit is meant to explore enabling CNAME-lookup by default for the next stable release while: - Eliminating a development burden by removing the need to create a new filtering syntax to deal with undesirable CNAME-cloaked hostnames - Eliminating a filter list maintainer burden by removing the need to find/deal with all base domains which engage in undesirable CNAME-cloaked hostnames The hope is that the approach implemented in this commit should require at most a few unbreak rules with no further need for special filtering syntax or filter list maintance efforts.
This commit is contained in:
parent
a817c8056e
commit
a16e4161de
@ -1259,18 +1259,29 @@ vAPI.Net = class {
|
|||||||
console.info('No requests found to benchmark');
|
console.info('No requests found to benchmark');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const mappedTypes = new Map([
|
||||||
|
[ 'document', 'main_frame' ],
|
||||||
|
[ 'subdocument', 'sub_frame' ],
|
||||||
|
]);
|
||||||
console.info('vAPI.net.onBeforeSuspendableRequest()...');
|
console.info('vAPI.net.onBeforeSuspendableRequest()...');
|
||||||
const t0 = self.performance.now();
|
const t0 = self.performance.now();
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for ( const request of requests ) {
|
|
||||||
const details = {
|
const details = {
|
||||||
documentUrl: request.frameUrl,
|
documentUrl: '',
|
||||||
tabId: Number.MAX_SAFE_INTEGER,
|
tabId: -1,
|
||||||
parentFrameId: -1,
|
parentFrameId: -1,
|
||||||
frameId: 0,
|
frameId: 0,
|
||||||
type: request.cpt,
|
type: '',
|
||||||
url: request.url,
|
url: '',
|
||||||
};
|
};
|
||||||
|
for ( const request of requests ) {
|
||||||
|
details.documentUrl = request.frameUrl;
|
||||||
|
details.tabId = -1;
|
||||||
|
details.parentFrameId = -1;
|
||||||
|
details.frameId = 0;
|
||||||
|
details.type = mappedTypes.get(request.cpt) || request.cpt;
|
||||||
|
details.url = request.url;
|
||||||
|
if ( details.type === 'main_frame' ) { continue; }
|
||||||
promises.push(this.onBeforeSuspendableRequest(details));
|
promises.push(this.onBeforeSuspendableRequest(details));
|
||||||
}
|
}
|
||||||
return Promise.all(promises).then(results => {
|
return Promise.all(promises).then(results => {
|
||||||
|
@ -63,16 +63,20 @@
|
|||||||
this.cnames = new Map([ [ '', '' ] ]);
|
this.cnames = new Map([ [ '', '' ] ]);
|
||||||
this.cnameAliasList = null;
|
this.cnameAliasList = null;
|
||||||
this.cnameIgnoreList = null;
|
this.cnameIgnoreList = null;
|
||||||
this.url = new URL(vAPI.getURL('/'));
|
this.cnameIgnore1stParty = true;
|
||||||
|
this.cnameIgnoreRootDocument = true;
|
||||||
this.cnameMaxTTL = 60;
|
this.cnameMaxTTL = 60;
|
||||||
|
this.cnameReplayFullURL = false;
|
||||||
this.cnameTimer = undefined;
|
this.cnameTimer = undefined;
|
||||||
}
|
}
|
||||||
setOptions(options) {
|
setOptions(options) {
|
||||||
super.setOptions(options);
|
super.setOptions(options);
|
||||||
this.cnameAliasList = this.regexFromStrList(options.cnameAliasList);
|
this.cnameAliasList = this.regexFromStrList(options.cnameAliasList);
|
||||||
this.cnameIgnoreList = this.regexFromStrList(options.cnameIgnoreList);
|
this.cnameIgnoreList = this.regexFromStrList(options.cnameIgnoreList);
|
||||||
this.cnameIgnore1stParty = options.cnameIgnore1stParty === true;
|
this.cnameIgnore1stParty = options.cnameIgnore1stParty !== false;
|
||||||
|
this.cnameIgnoreRootDocument = options.cnameIgnoreRootDocument !== false;
|
||||||
this.cnameMaxTTL = options.cnameMaxTTL || 120;
|
this.cnameMaxTTL = options.cnameMaxTTL || 120;
|
||||||
|
this.cnameReplayFullURL = options.cnameReplayFullURL === true;
|
||||||
this.cnames.clear(); this.cnames.set('', '');
|
this.cnames.clear(); this.cnames.set('', '');
|
||||||
}
|
}
|
||||||
normalizeDetails(details) {
|
normalizeDetails(details) {
|
||||||
@ -123,11 +127,22 @@
|
|||||||
}
|
}
|
||||||
return Array.from(out);
|
return Array.from(out);
|
||||||
}
|
}
|
||||||
processCanonicalName(cname, details) {
|
processCanonicalName(hn, cn, details) {
|
||||||
this.url.href = details.url;
|
const hnBeg = details.url.indexOf(hn);
|
||||||
details.cnameOf = this.url.hostname;
|
if ( hnBeg === -1 ) { return; }
|
||||||
this.url.hostname = cname;
|
const oldURL = details.url;
|
||||||
details.url = this.url.href;
|
let newURL = oldURL.slice(0, hnBeg) + cn;
|
||||||
|
const hnEnd = hnBeg + hn.length;
|
||||||
|
if ( this.cnameReplayFullURL ) {
|
||||||
|
newURL += oldURL.slice(hnEnd);
|
||||||
|
} else {
|
||||||
|
const pathBeg = oldURL.indexOf('/', hnEnd);
|
||||||
|
if ( pathBeg !== -1 ) {
|
||||||
|
newURL += oldURL.slice(hnEnd, pathBeg + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
details.url = newURL;
|
||||||
|
details.aliasURL = oldURL;
|
||||||
return super.onBeforeSuspendableRequest(details);
|
return super.onBeforeSuspendableRequest(details);
|
||||||
}
|
}
|
||||||
recordCanonicalName(hn, record) {
|
recordCanonicalName(hn, record) {
|
||||||
@ -187,11 +202,14 @@
|
|||||||
let r = super.onBeforeSuspendableRequest(details);
|
let r = super.onBeforeSuspendableRequest(details);
|
||||||
if ( r !== undefined ) { return r; }
|
if ( r !== undefined ) { return r; }
|
||||||
if ( this.cnameAliasList === null ) { return; }
|
if ( this.cnameAliasList === null ) { return; }
|
||||||
|
if ( details.type === 'main_frame' && this.cnameIgnoreRootDocument ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const hn = vAPI.hostnameFromNetworkURL(details.url);
|
const hn = vAPI.hostnameFromNetworkURL(details.url);
|
||||||
let cname = this.cnames.get(hn);
|
let cname = this.cnames.get(hn);
|
||||||
if ( cname === '' ) { return; }
|
if ( cname === '' ) { return; }
|
||||||
if ( cname !== undefined ) {
|
if ( cname !== undefined ) {
|
||||||
return this.processCanonicalName(cname, details);
|
return this.processCanonicalName(hn, cname, details);
|
||||||
}
|
}
|
||||||
if ( this.cnameAliasList.test(hn) === false ) {
|
if ( this.cnameAliasList.test(hn) === false ) {
|
||||||
this.cnames.set(hn, '');
|
this.cnames.set(hn, '');
|
||||||
@ -201,7 +219,7 @@
|
|||||||
rec => {
|
rec => {
|
||||||
const cname = this.recordCanonicalName(hn, rec);
|
const cname = this.recordCanonicalName(hn, rec);
|
||||||
if ( cname === '' ) { return; }
|
if ( cname === '' ) { return; }
|
||||||
return this.processCanonicalName(cname, details);
|
return this.processCanonicalName(hn, cname, details);
|
||||||
},
|
},
|
||||||
( ) => {
|
( ) => {
|
||||||
this.cnames.set(hn, '');
|
this.cnames.set(hn, '');
|
||||||
|
@ -268,7 +268,7 @@ body.colorBlind #vwRenderer .logEntry > div.cosmeticRealm,
|
|||||||
body.colorBlind #vwRenderer .logEntry > div.redirect {
|
body.colorBlind #vwRenderer .logEntry > div.redirect {
|
||||||
background-color: rgba(0, 19, 110, 0.1);
|
background-color: rgba(0, 19, 110, 0.1);
|
||||||
}
|
}
|
||||||
#vwRenderer .logEntry > div[data-cnameof] {
|
#vwRenderer .logEntry > div[data-aliasid] {
|
||||||
color: mediumblue;
|
color: mediumblue;
|
||||||
}
|
}
|
||||||
#vwRenderer .logEntry > div[data-type="tabLoad"] {
|
#vwRenderer .logEntry > div[data-type="tabLoad"] {
|
||||||
|
@ -49,7 +49,9 @@ const µBlock = (( ) => { // jshint ignore:line
|
|||||||
cnameAliasList: 'unset',
|
cnameAliasList: 'unset',
|
||||||
cnameIgnoreList: 'unset',
|
cnameIgnoreList: 'unset',
|
||||||
cnameIgnore1stParty: true,
|
cnameIgnore1stParty: true,
|
||||||
|
cnameIgnoreRootDocument: true,
|
||||||
cnameMaxTTL: 120,
|
cnameMaxTTL: 120,
|
||||||
|
cnameReplayFullURL: false,
|
||||||
consoleLogLevel: 'unset',
|
consoleLogLevel: 'unset',
|
||||||
debugScriptlets: false,
|
debugScriptlets: false,
|
||||||
debugScriptletInjector: false,
|
debugScriptletInjector: false,
|
||||||
|
@ -29,9 +29,10 @@
|
|||||||
}
|
}
|
||||||
this.tstamp = 0;
|
this.tstamp = 0;
|
||||||
this.realm = '';
|
this.realm = '';
|
||||||
|
this.id = undefined;
|
||||||
this.type = undefined;
|
this.type = undefined;
|
||||||
this.cnameOf = undefined;
|
|
||||||
this.url = undefined;
|
this.url = undefined;
|
||||||
|
this.aliasURL = undefined;
|
||||||
this.hostname = undefined;
|
this.hostname = undefined;
|
||||||
this.domain = undefined;
|
this.domain = undefined;
|
||||||
this.docId = undefined;
|
this.docId = undefined;
|
||||||
@ -64,9 +65,10 @@
|
|||||||
}
|
}
|
||||||
this.fromTabId(tabId);
|
this.fromTabId(tabId);
|
||||||
this.realm = '';
|
this.realm = '';
|
||||||
|
this.id = details.requestId;
|
||||||
this.type = details.type;
|
this.type = details.type;
|
||||||
this.setURL(details.url);
|
this.setURL(details.url);
|
||||||
this.cnameOf = details.cnameOf || undefined;
|
this.aliasURL = details.aliasURL || undefined;
|
||||||
this.docId = details.type !== 'sub_frame'
|
this.docId = details.type !== 'sub_frame'
|
||||||
? details.frameId
|
? details.frameId
|
||||||
: details.parentFrameId;
|
: details.parentFrameId;
|
||||||
|
@ -157,9 +157,12 @@ const regexFromURLFilteringResult = function(result) {
|
|||||||
|
|
||||||
const nodeFromURL = function(parent, url, re) {
|
const nodeFromURL = function(parent, url, re) {
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
if ( re instanceof RegExp === false ) {
|
if ( re === undefined ) {
|
||||||
fragment.textContent = url;
|
fragment.textContent = url;
|
||||||
} else {
|
} else {
|
||||||
|
if ( typeof re === 'string' ) {
|
||||||
|
re = new RegExp(re.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
|
||||||
|
}
|
||||||
const matches = re.exec(url);
|
const matches = re.exec(url);
|
||||||
if ( matches === null || matches[0].length === 0 ) {
|
if ( matches === null || matches[0].length === 0 ) {
|
||||||
fragment.textContent = url;
|
fragment.textContent = url;
|
||||||
@ -211,6 +214,9 @@ const LogEntry = function(details) {
|
|||||||
this[prop] = details[prop];
|
this[prop] = details[prop];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ( details.aliasURL !== undefined ) {
|
||||||
|
this.aliased = true;
|
||||||
|
}
|
||||||
if ( this.tabDomain === '' ) {
|
if ( this.tabDomain === '' ) {
|
||||||
this.tabDomain = this.tabHostname || '';
|
this.tabDomain = this.tabHostname || '';
|
||||||
}
|
}
|
||||||
@ -222,12 +228,13 @@ const LogEntry = function(details) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
LogEntry.prototype = {
|
LogEntry.prototype = {
|
||||||
cnameOf: '',
|
aliased: false,
|
||||||
dead: false,
|
dead: false,
|
||||||
docDomain: '',
|
docDomain: '',
|
||||||
docHostname: '',
|
docHostname: '',
|
||||||
domain: '',
|
domain: '',
|
||||||
filter: undefined,
|
filter: undefined,
|
||||||
|
id: '',
|
||||||
realm: '',
|
realm: '',
|
||||||
tabDomain: '',
|
tabDomain: '',
|
||||||
tabHostname: '',
|
tabHostname: '',
|
||||||
@ -294,7 +301,7 @@ const processLoggerEntries = function(response) {
|
|||||||
if ( autoDeleteVoidedRows ) { continue; }
|
if ( autoDeleteVoidedRows ) { continue; }
|
||||||
parsed.voided = true;
|
parsed.voided = true;
|
||||||
}
|
}
|
||||||
if ( parsed.type === 'main_frame' && parsed.cnameOf === '' ) {
|
if ( parsed.type === 'main_frame' && parsed.aliased === false ) {
|
||||||
const separator = createLogSeparator(parsed, unboxed.url);
|
const separator = createLogSeparator(parsed, unboxed.url);
|
||||||
loggerEntries.unshift(separator);
|
loggerEntries.unshift(separator);
|
||||||
if ( rowFilterer.filterOne(separator) ) {
|
if ( rowFilterer.filterOne(separator) ) {
|
||||||
@ -304,7 +311,7 @@ const processLoggerEntries = function(response) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ( cnameOfEnabled === false && parsed.cnameOf !== '' ) {
|
if ( cnameOfEnabled === false && parsed.aliased ) {
|
||||||
uDom.nodeFromId('filterExprCnameOf').style.display = '';
|
uDom.nodeFromId('filterExprCnameOf').style.display = '';
|
||||||
cnameOfEnabled = true;
|
cnameOfEnabled = true;
|
||||||
}
|
}
|
||||||
@ -405,8 +412,10 @@ const parseLogEntry = function(details) {
|
|||||||
textContent.push(normalizeToStr(details.url));
|
textContent.push(normalizeToStr(details.url));
|
||||||
|
|
||||||
// Hidden cells -- useful for row-filtering purpose
|
// Hidden cells -- useful for row-filtering purpose
|
||||||
if ( entry.cnameOf !== '' ) {
|
|
||||||
textContent.push(`cnameOf=${entry.cnameOf}`);
|
// Cell 7
|
||||||
|
if ( entry.aliased ) {
|
||||||
|
textContent.push(`aliasURL=${details.aliasURL}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.textContent = textContent.join('\t');
|
entry.textContent = textContent.join('\t');
|
||||||
@ -723,7 +732,7 @@ const viewPort = (( ) => {
|
|||||||
span.textContent = cells[5];
|
span.textContent = cells[5];
|
||||||
|
|
||||||
// URL
|
// URL
|
||||||
let re = null;
|
let re;
|
||||||
if ( filteringType === 'static' ) {
|
if ( filteringType === 'static' ) {
|
||||||
re = new RegExp(filter.regex, 'gi');
|
re = new RegExp(filter.regex, 'gi');
|
||||||
} else if ( filteringType === 'dynamicUrl' ) {
|
} else if ( filteringType === 'dynamicUrl' ) {
|
||||||
@ -731,9 +740,12 @@ const viewPort = (( ) => {
|
|||||||
}
|
}
|
||||||
nodeFromURL(div.children[6], cells[6], re);
|
nodeFromURL(div.children[6], cells[6], re);
|
||||||
|
|
||||||
// Cname
|
// Alias URL (CNAME, etc.)
|
||||||
if ( details.cnameOf !== '' ) {
|
if ( cells.length > 7 ) {
|
||||||
div.setAttribute('data-cnameof', details.cnameOf);
|
const pos = details.textContent.lastIndexOf('\taliasURL=');
|
||||||
|
if ( pos !== -1 ) {
|
||||||
|
div.setAttribute('data-aliasid', details.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return div;
|
return div;
|
||||||
@ -1452,6 +1464,16 @@ const reloadTab = function(ev) {
|
|||||||
return targetRow.children[1].textContent;
|
return targetRow.children[1].textContent;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const aliasURLFromID = function(id) {
|
||||||
|
if ( id === '' ) { return ''; }
|
||||||
|
for ( const entry of loggerEntries ) {
|
||||||
|
if ( entry.id !== id || entry.aliased ) { continue; }
|
||||||
|
const fields = entry.textContent.split('\t');
|
||||||
|
return fields[6] || '';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
const toSummaryPaneFilterNode = async function(receiver, filter) {
|
const toSummaryPaneFilterNode = async function(receiver, filter) {
|
||||||
receiver.children[1].textContent = filter;
|
receiver.children[1].textContent = filter;
|
||||||
if ( filterAuthorMode !== true ) { return; }
|
if ( filterAuthorMode !== true ) { return; }
|
||||||
@ -1613,8 +1635,8 @@ const reloadTab = function(ev) {
|
|||||||
rows[6].style.display = 'none';
|
rows[6].style.display = 'none';
|
||||||
}
|
}
|
||||||
// URL
|
// URL
|
||||||
text = trch[6].textContent;
|
const canonicalURL = trch[6].textContent;
|
||||||
if ( text !== '' ) {
|
if ( canonicalURL !== '' ) {
|
||||||
const attr = tr.getAttribute('data-status') || '';
|
const attr = tr.getAttribute('data-status') || '';
|
||||||
if ( attr !== '' ) {
|
if ( attr !== '' ) {
|
||||||
rows[7].setAttribute('data-status', attr);
|
rows[7].setAttribute('data-status', attr);
|
||||||
@ -1623,12 +1645,17 @@ const reloadTab = function(ev) {
|
|||||||
} else {
|
} else {
|
||||||
rows[7].style.display = 'none';
|
rows[7].style.display = 'none';
|
||||||
}
|
}
|
||||||
// CNAME of
|
// Alias URL
|
||||||
text = tr.getAttribute('data-cnameof') || '';
|
text = tr.getAttribute('data-aliasid');
|
||||||
if ( text !== '' ) {
|
const aliasURL = text ? aliasURLFromID(text) : '';
|
||||||
rows[8].children[1].textContent = text;
|
if ( aliasURL !== '' ) {
|
||||||
|
rows[8].children[1].textContent =
|
||||||
|
vAPI.hostnameFromURI(aliasURL) + ' \u21d2\n\u2003' +
|
||||||
|
vAPI.hostnameFromURI(canonicalURL);
|
||||||
|
rows[9].children[1].textContent = aliasURL;
|
||||||
} else {
|
} else {
|
||||||
rows[8].style.display = 'none';
|
rows[8].style.display = 'none';
|
||||||
|
rows[9].style.display = 'none';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -139,7 +139,9 @@ self.addEventListener('hiddenSettingsChanged', ( ) => {
|
|||||||
cnameAliasList: µBlock.hiddenSettings.cnameAliasList,
|
cnameAliasList: µBlock.hiddenSettings.cnameAliasList,
|
||||||
cnameIgnoreList: µBlock.hiddenSettings.cnameIgnoreList,
|
cnameIgnoreList: µBlock.hiddenSettings.cnameIgnoreList,
|
||||||
cnameIgnore1stParty: µBlock.hiddenSettings.cnameIgnore1stParty,
|
cnameIgnore1stParty: µBlock.hiddenSettings.cnameIgnore1stParty,
|
||||||
|
cnameIgnoreRootDocument: µBlock.hiddenSettings.cnameIgnoreRootDocument,
|
||||||
cnameMaxTTL: µBlock.hiddenSettings.cnameMaxTTL,
|
cnameMaxTTL: µBlock.hiddenSettings.cnameMaxTTL,
|
||||||
|
cnameReplayFullURL: µBlock.hiddenSettings.cnameReplayFullURL,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ const onBeforeRequest = function(details) {
|
|||||||
if (
|
if (
|
||||||
details.parentFrameId !== -1 &&
|
details.parentFrameId !== -1 &&
|
||||||
details.type === 'sub_frame' &&
|
details.type === 'sub_frame' &&
|
||||||
details.cnameOf === undefined
|
details.aliasURL === undefined
|
||||||
) {
|
) {
|
||||||
pageStore.setFrame(details.frameId, details.url);
|
pageStore.setFrame(details.frameId, details.url);
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div><span data-filtex="!" data-i18n="loggerRowFiltererBuiltinNot"></span><span data-filtex="\t(?:0,)?1\t" data-i18n="loggerRowFiltererBuiltin1p"></span><span data-filtex="\t(?:3(?:,\d)?|0,3)\t" data-i18n="loggerRowFiltererBuiltin3p"></span></div>
|
<div><span data-filtex="!" data-i18n="loggerRowFiltererBuiltinNot"></span><span data-filtex="\t(?:0,)?1\t" data-i18n="loggerRowFiltererBuiltin1p"></span><span data-filtex="\t(?:3(?:,\d)?|0,3)\t" data-i18n="loggerRowFiltererBuiltin3p"></span></div>
|
||||||
<div id="filterExprCnameOf" style="display:none"><span data-filtex="!" data-i18n="loggerRowFiltererBuiltinNot"></span><span data-filtex="\tcnameOf=.">CNAME</span></div>
|
<div id="filterExprCnameOf" style="display:none"><span data-filtex="!" data-i18n="loggerRowFiltererBuiltinNot"></span><span data-filtex="\taliasURL=.">CNAME</span></div>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@ -121,7 +121,8 @@
|
|||||||
<div><span data-i18n="loggerEntryDetailsPartyness"></span><span class="prose"></span></div>
|
<div><span data-i18n="loggerEntryDetailsPartyness"></span><span class="prose"></span></div>
|
||||||
<div><span data-i18n="loggerEntryDetailsType"></span><span></span></div>
|
<div><span data-i18n="loggerEntryDetailsType"></span><span></span></div>
|
||||||
<div><span data-i18n="loggerEntryDetailsURL"></span><span></span></div>
|
<div><span data-i18n="loggerEntryDetailsURL"></span><span></span></div>
|
||||||
<div><span >CNAME of</span><span></span></div>
|
<div><span>CNAME</span><span></span></div>
|
||||||
|
<div><span>Original URL</span><span></span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pane dynamic hide" data-pane="dynamic">
|
<div class="pane dynamic hide" data-pane="dynamic">
|
||||||
<div class="toolbar row">
|
<div class="toolbar row">
|
||||||
|
Loading…
Reference in New Issue
Block a user