DOM mutator cleanup

This commit is contained in:
Mr. Stallion 2020-04-05 13:29:43 -05:00
parent 00ab298d62
commit c7b8b53f9a
9 changed files with 441 additions and 236 deletions

View File

@ -39,7 +39,7 @@
import core from '../core';
import { EventBus, EventBusEvent } from './event-bus';
import {domain} from '../../bbcode/core';
import {ImagePreviewMutator} from './image-preview-mutator';
import {ImageDomMutator} from './image-dom-mutator';
import { ExternalImagePreviewHelper, LocalImagePreviewHelper } from './helper';
@ -77,7 +77,7 @@
runJs = true;
debug = false;
jsMutator = new ImagePreviewMutator(this.debug);
jsMutator = new ImageDomMutator(this.debug);
externalPreviewStyle: Record<string, any> = {};
localPreviewStyle: Record<string, any> = {};
@ -101,6 +101,9 @@
onMounted(): void {
console.warn('Mounted ImagePreview');
// tslint:disable-next-line:no-floating-promises
this.jsMutator.init();
EventBus.$on(
'imagepreview-dismiss',
(eventData: EventBusEvent) => {
@ -572,7 +575,7 @@
this.runJs = true;
this.debug = false;
this.jsMutator = new ImagePreviewMutator(this.debug);
this.jsMutator = new ImageDomMutator(this.debug);
this.cancelExitTimer();
this.cancelTimer();

View File

@ -1,12 +1,20 @@
// window.onload = () => console.log('window.onload');
// window.onloadstart = () => console.log('window.onloadstart');
// window.onloadend = () => console.log('window.onloadend');
// window.addEventListener('DOMContentLoaded', () => (console.log('window.DOMContentLoaded')));
// setTimeout(() => (console.log('Timeout')), 0); ---- Note that clear() below will break this
/***
* This script is injected on every web page ImagePreview loads
*/
const previewInitiationTime = Date.now();
window.onload = () => console.log('window.onload', `${(Date.now() - previewInitiationTime)/1000}s`);
window.onloadstart = () => console.log('window.onloadstart', `${(Date.now() - previewInitiationTime)/1000}s`);
window.onloadend = () => console.log('window.onloadend', `${(Date.now() - previewInitiationTime)/1000}s`);
window.addEventListener('DOMContentLoaded', () => (console.log('window.DOMContentLoaded', `${(Date.now() - previewInitiationTime)/1000}s`)));
setTimeout(() => (console.log('Timeout', `${(Date.now() - previewInitiationTime)/1000}s`)), 0); // ---- Note that clear() below could break this
(() => {
try {
if (window.location.href.match(/^https?:\/\/(www.)?pornhub.com/)) {
// Inject JQuery
const el = document.createElement('script');
el.type='text/javascript';
el.text="console.log('JQuery Injection'); window.$ = window.jQuery = require('jquery');";

View File

@ -0,0 +1,365 @@
/*
This script is MUTATED and EXECUTED after DOM has loaded
It is wrapped into a `(() => {})();` to prevent it from polluting its surroundings.
Avoid using array functions, such as `arr.forEach`, as some websites override them with incompatible functions
Do not remove the `SETTINGS_START` and `SETTINGS_END` markers below,
they are used for dynamically injecting settings from Electron.
*/
const sizePairs = [
['naturalWidth', 'naturalHeight'],
['videoWidth', 'videoHeight'],
['width', 'height'],
];
class FListImagePreviewDomMutator {
constructor(settings) {
/* ## SETTINGS_START ## */
this.settings = settings || {
selectors: ['video', 'img'],
debug: true,
skipElementRemove: false,
safeTags: [],
injectStyle: false
};
/* ## SETTINGS_END ## */
this.startTime = Date.now();
this.selectors = this.settings.selectors;
this.skipElementRemove = this.settings.skipElementRemove;
this.safeTags = this.settings.safeTags;
this.body = document.querySelector('body');
this.html = document.querySelector('html');
this.ipcRenderer = (typeof require !== 'undefined')
? require('electron').ipcRenderer
: { sendToHost: (...args) => (this.debug('ipc.sendToHost', ...args)) };
this.preprocess();
this.img = this.detectImage(this.selectors, this.body);
this.wrapper = this.createWrapperElement();
this.style = this.createStyleElement();
}
preprocess() {
for (const el of document.querySelectorAll('header, .header, nav, .nav, .navbar, .navigation')) {
try {
el.remove();
} catch (err) {
this.error('preprocess', err);
}
}
}
detectImage(selectors, body) {
let selected = [];
for (const selector of selectors) {
const selectedElements = (Array.from(document.querySelectorAll(selector)).filter((i) => ((i.width !== 1) && (i.height !== 1))));
selected = selected.concat(selectedElements);
}
this.debug('detectImage.selected', selectors, selected);
const img = selected.filter(el => (el !== body)).shift();
this.debug('detectImage.found', img);
return img;
}
run() {
if (!this.img) {
return;
}
this.updateImgSize(this.img, 'pre');
this.attachImgToWrapper(this.img, this.wrapper);
this.attachWrapperToBody(this.wrapper, this.body);
this.attachStyleToWrapper(this.style, this.wrapper);
this.forceElementStyling(this.html, this.body, this.wrapper, this.img);
this.resolveVideoSrc(this.img);
this.setEventListener('DOMContentLoaded', this.img);
this.setEventListener('load', this.img);
this.setEventListener('loadstart', this.img);
this.attemptPlay(this.img, true);
this.updateImgSizeTimer(this.img);
this.cleanDom(this.body);
}
cleanDom(body) {
if (this.skipElementRemove) {
return;
}
const removeList = [];
const safeIds = ['flistWrapper', 'flistError', 'flistHider'];
const safeTags = this.safeTags;
for (const el of body.childNodes) {
try {
if (
(safeIds.indexOf(el.id) < 0)
&& ((!el.tagName) || (safeTags.indexOf(el.tagName.toLowerCase())) < 0)
) {
removeList.push(el);
}
} catch (err) {
this.error('cleanDom find nodes', err);
}
}
for (const el of removeList) {
try {
el.remove();
} catch (err) {
this.error('cleanDom remove element', err);
}
}
}
updateImgSizeTimer(img) {
const result = this.updateImgSize(img, 'timer');
if (!result) {
setTimeout(() => this.updateImgSizeTimer(img), 100);
}
}
resolveVideoSrc(img) {
if ((img.src) || (!img.tagName) || ((img.tagName) && (img.tagName.toUpperCase() !== 'VIDEO'))) {
return;
}
this.debug('resolveVideoSrc', 'Needs a content URL', img);
const contentUrls = document.querySelectorAll('meta[itemprop="contentURL"]');
if ((contentUrls) && (contentUrls.length > 0)) {
this.debug('Found content URLs', contentUrls);
const cu = contentUrls[0];
if ((cu.attributes) && (cu.attributes.content) && (cu.attributes.content.value)) {
this.debug('Content URL', cu.attributes.content.value);
img.src = cu.attributes.content.value;
}
}
}
setEventListener(eventName, img) {
document.addEventListener(eventName, (event) => {
this.debug('event', eventName, event);
this.updateImgSize(img, `event.${eventName}`);
this.attemptPlay(img, false);
});
}
attemptPlay(img, lessStrict) {
try {
if (
(img.play)
&& (
(lessStrict)
|| ((!lessStrict) && (!img.paused) && (!img.ended) && (!(img.currentTime > 0)))
)
)
{
img.muted = true;
img.loop = true;
img.play();
}
} catch (err) {
this.error('attemptPlay', err, img, lessStrict);
}
}
forceElementStyling(html, body, wrapper, img) {
try {
body.class = '';
img.class = '';
wrapper.class = '';
html.class = '';
body.removeAttribute('class');
img.removeAttribute('class');
wrapper.removeAttribute('class');
html.removeAttribute('class');
img.removeAttribute('width');
img.removeAttribute('height');
} catch (err) {
this.error('forceElementStyling remove class', err);
}
html.style = this.getWrapperStyleOverrides();
body.style = this.getWrapperStyleOverrides();
img.style = this.getImageStyleOverrides();
}
attachWrapperToBody(wrapper, body) {
body.append(wrapper);
}
attachStyleToWrapper(style, wrapper) {
try {
wrapper.append(style);
} catch (err) {
this.error('attach style', err);
}
}
attachImgToWrapper(img, wrapper) {
try {
img.remove();
} catch(err) {
this.error('attachImgToWrapper', 'remove()', err);
try {
img.parentNode.removeChild(img);
} catch(err2) {
console.error('attachImgToWrapper', 'removeChild()', err2);
}
}
wrapper.append(img);
}
createWrapperElement() {
const el = document.createElement('div');
el.id = 'flistWrapper';
el.style = this.getWrapperStyleOverrides()
+ 'z-index: 100000 !important;'
+ 'background-color: black !important;'
+ 'background-size: contain !important;'
+ 'background-repeat: no-repeat !important;'
+ 'background-position: top left !important;';
return el;
}
createStyleElement() {
if (!!this.settings.skipInjectStyle) {
return document.createElement('i');
}
const el = document.createElement('style');
el.textContent = `
#flistWrapper img, #flistWrapper video {
${this.getImageStyleOverrides()}
}
`;
return el;
}
resolveImgSize(img) {
const solved = {};
for (let ri = 0; ri < sizePairs.length; ri++) {
const val = sizePairs[ri];
if ((img[val[0]]) && (img[val[1]])) {
solved.width = img[val[0]];
solved.height = img[val[1]];
break;
}
}
return solved;
}
updateImgSize(img, stage) {
const imSize = this.resolveImgSize(img);
if ((imSize.width) && (imSize.height)) {
this.debug('IPC webview.img', imSize, stage);
this.ipcRenderer.sendToHost('webview.img', imSize.width, imSize.height, stage);
return true;
}
return false;
}
getBasicStyleOverrides() {
return 'border: 0 !important;'
+ 'padding: 0 !important;'
+ 'margin: 0 !important;'
+ 'width: 100% !important;'
+ 'height: 100% !important;'
+ 'opacity: 1 !important;'
+ 'min-width: initial !important;'
+ 'min-height: initial !important;'
+ 'max-width: initial !important;'
+ 'max-height: initial !important;'
+ 'display: block !important;'
+ 'visibility: visible !important;';
}
getWrapperStyleOverrides() {
return this.getBasicStyleOverrides()
+ 'overflow: hidden !important;'
+ 'top: 0 !important;'
+ 'left: 0 !important;'
+ 'position: absolute !important;';
}
getImageStyleOverrides() {
return this.getBasicStyleOverrides()
+ 'object-position: top left !important;'
+ 'object-fit: contain !important;';
}
debug(...args) {
if (this.settings.debug) {
console.log('DOM Mutator:', ...args, `${(Date.now() - this.startTime)/1000}s`);
}
}
error(...args) {
console.error('DOM Mutator:', ...args, `${(Date.now() - this.startTime)/1000}s`);
}
}
/* ## EXECUTION_START ## */
const flistImagePreviewMutator = new FListImagePreviewDomMutator();
flistImagePreviewMutator.run();
/* ## EXECUTION_END ## */

View File

@ -3,10 +3,15 @@
import * as _ from 'lodash';
import * as urlHelper from 'url';
import { domain as extractDomain } from '../../bbcode/core';
export interface PreviewMutator {
// tslint:disable-next-line:ban-ts-ignore
// @ts-ignore
// tslint:disable-next-line:no-submodule-imports ban-ts-ignore match-default-export-name
import processorScript from '!!raw-loader!./assets/browser.processor.raw.js';
export interface DomMutator {
match: string | RegExp;
injectJs: string;
eventName: string;
@ -14,32 +19,31 @@ export interface PreviewMutator {
urlMutator?(url: string): string;
}
export interface ImagePreviewMutatorCollection {
[key: string]: PreviewMutator;
}
// tslint:disable-next-line:max-line-length
const imgurOuterStyle = 'z-index: 1000000; position: absolute; bottom: 0.75rem; right: 0.75rem; background: rgba(0, 128, 0, 0.8); border: 2px solid rgba(144, 238, 144, 0.5); width: 3rem; height: 3rem; font-size: 15pt; font-weight: normal; color: white; border-radius: 3rem; margin: 0; font-family: Helvetica,Arial,sans-serif; box-shadow: 2px 2px 2px rgba(0,0,0,0.5)';
// tslint:disable-next-line:max-line-length
const imgurInnerStyle = 'position: absolute; top: 50%; left: 50%; transform: translateY(-50%) translateX(-50%); text-shadow: 1px 1px 2px rgba(0,0,0,0.4);';
export interface DomMutatorScripts {
processor: string;
}
export class ImagePreviewMutator {
export class ImageDomMutator {
// tslint:disable: prefer-function-over-method
private hostMutators: ImagePreviewMutatorCollection = {};
private regexMutators: PreviewMutator[] = [];
private hostMutators: Record<string, DomMutator> = {};
private regexMutators: DomMutator[] = [];
private debug: boolean;
private scripts: DomMutatorScripts = { processor: '' };
constructor(debug: boolean) {
this.init();
// this.debug = debug;
this.debug = debug || true;
}
setDebug(debug: boolean): void {
this.debug = debug;
this.debug = debug || true;
}
@ -65,7 +69,7 @@ export class ImagePreviewMutator {
return this.wrapJs(mutator.injectJs) + this.getReShowMutator();
}
matchMutator(url: string): PreviewMutator | undefined {
matchMutator(url: string): DomMutator | undefined {
const urlDomain = extractDomain(url);
if (!urlDomain)
@ -76,7 +80,7 @@ export class ImagePreviewMutator {
return _.find(
this.regexMutators,
(m: PreviewMutator) => {
(m: DomMutator) => {
const match = m.match;
return (match instanceof RegExp) ? (urlDomain.match(match) !== null) : (match === urlDomain);
@ -115,7 +119,17 @@ export class ImagePreviewMutator {
};
}
protected init(): void {
protected async loadScripts(): Promise<void> {
this.scripts = {
processor: processorScript
};
}
async init(): Promise<void> {
await this.loadScripts();
this.add('default', this.getBaseJsMutatorScript(['#video, video', '#image, img']));
this.add('e621.net', this.getBaseJsMutatorScript(['video', '#image']));
this.add('e-hentai.org', this.getBaseJsMutatorScript(['video', '#img']));
@ -139,6 +153,7 @@ export class ImagePreviewMutator {
this.add('tenor.com', this.getBaseJsMutatorScript(['#view video', '#view img']));
this.add('hypnohub.net', this.getBaseJsMutatorScript(['video', '#image', 'img']));
this.add('derpibooru.org', this.getBaseJsMutatorScript(['video', '#image-display', 'img']));
this.add('sexbot.gallery', this.getBaseJsMutatorScript(['video.hero', 'video']));
this.add(
'pornhub.com',
@ -208,223 +223,23 @@ export class ImagePreviewMutator {
);
}
getBaseJsMutatorScript(elSelector: string[], skipElementRemove: boolean = false, safeTags: string[] = []): string {
return `
const ipcRenderer = (typeof require !== 'undefined')
? require('electron').ipcRenderer
: { sendToHost: (...args) => (console.log('ipc.sendToHost', ...args)) };
const js = this.scripts.processor; // ./assets/browser.processor.raw.js
const body = document.querySelector('body');
const html = document.querySelector('html');
const selectors = ${JSON.stringify(elSelector)};
const settings = {
skipElementRemove,
safeTags,
selectors: elSelector,
debug: this.debug
};
for (const el of document.querySelectorAll('header, .header')) {
try {
el.remove();
} catch (err) {
console.error('Header removal error', err);
}
}
const settingsJson = JSON.stringify(settings, null, 0);
// writing this out because sometimes .map and .reduce are overridden
let selected = [];
for (selector of selectors) {
const selectedElements = (Array.from(document.querySelectorAll(selector)).filter((i) => ((i.width !== 1) && (i.height !== 1))));
selected = selected.concat(selectedElements);
}
${this.debug ? `console.log('Selector', '${elSelector.toString()}'); console.log('Selected', selected);` : ''}
const img = selected.filter(el => (el !== body)).shift();
${this.debug ? `console.log('Img', img);` : ''}
if (!img) { return; }
const sizePairs = [
['naturalWidth', 'naturalHeight'],
['videoWidth', 'videoHeight'],
['width', 'height'],
];
const resolveImgSize = function() {
const solved = {};
for (let ri = 0; ri < sizePairs.length; ri++) {
const val = sizePairs[ri];
if ((img[val[0]]) && (img[val[1]])) {
solved.width = img[val[0]];
solved.height = img[val[1]];
break;
}
}
return solved;
}
const preImSize = resolveImgSize();
ipcRenderer.sendToHost('webview.img', preImSize.width, preImSize.height, 'preImSize');
const el = document.createElement('div');
el.id = 'flistWrapper';
el.style = 'width: 100% !important; height: 100% !important; position: absolute !important;'
+ 'top: 0 !important; left: 0 !important; z-index: 100000 !important;'
+ 'background-color: black !important; background-size: contain !important;'
+ 'background-repeat: no-repeat !important; background-position: top left !important;'
+ 'opacity: 1 !important; padding: 0 !important; border: 0 !important; margin: 0 !important;'
+ 'min-width: unset !important; min-height: unset !important; max-width: unset !important; max-height: unset !important;';
try {
img.remove();
} catch(err) {
console.error('Failed remove()', err);
try {
img.parentNode.removeChild(img);
} catch(err2) {
console.error('Failed removeChild()', err2);
}
}
el.append(img);
body.append(el);
body.class = '';
body.style = 'border: 0 !important; padding: 0 !important; margin: 0 !important; overflow: hidden !important;'
+ 'width: 100% !important; height: 100% !important; opacity: 1 !important;'
+ 'top: 0 !important; left: 0 !important; position: absolute !important;'
+ 'min-width: initial !important; min-height: initial !important; max-width: initial !important; max-height: initial !important;'
+ 'display: block !important; visibility: visible !important';
img.style = 'object-position: top left !important; object-fit: contain !important;'
+ 'width: 100% !important; height: 100% !important; opacity: 1 !important;'
+ 'margin: 0 !important; border: 0 !important; padding: 0 !important;'
+ 'min-width: initial !important; min-height: initial !important; max-width: initial !important; max-height: initial !important;'
+ 'display: block !important; visibility: visible !important;';
img.removeAttribute('width');
img.removeAttribute('height');
img.class = '';
el.class = '';
html.class = '';
html.style = 'border: 0 !important; padding: 0 !important; margin: 0 !important; overflow: hidden !important;'
+ 'width: 100% !important; height: 100% !important; opacity: 1 !important;'
+ 'top: 0 !important; left: 0 !important; position: absolute !important;'
+ 'min-width: initial !important; min-height: initial !important; max-width: initial !important; max-height: initial !important;'
+ 'display: block !important; visibility: visible !important';
const extraStyle = document.createElement('style');
extraStyle.textContent = \`
#flistWrapper img, #flistWrapper video {
object-position: top left !important;
object-fit: contain !important;
width: 100% !important;
height: 100% !important;
opacity: 1 !important;
margin: 0 !important;
border: 0 !important;
padding: 0 !important;
min-width: initial !important;
min-height: initial !important;
max-width: initial !important;
max-height: initial !important;
display: block !important;
visibility: visible !important;
}
\`;
el.append(extraStyle);
${this.debug ? "console.log('Wrapper', el);" : ''}
if ((!img.src) && (img.tagName) && (img.tagName.toUpperCase() === 'VIDEO')) {
${this.debug ? "console.log('Nedds a content URL', img);" : ''}
const contentUrls = document.querySelectorAll('meta[itemprop="contentURL"]');
if ((contentUrls) && (contentUrls.length > 0)) {
${this.debug ? "console.log('Found content URLs', contentUrls);" : ''}
const cu = contentUrls[0];
if ((cu.attributes) && (cu.attributes.content) && (cu.attributes.content.value)) {
${this.debug ? "console.log('Content URL', cu.attributes.content.value);" : ''}
img.src = cu.attributes.content.value;
}
}
}
document.addEventListener('DOMContentLoaded', (event) => {
${this.debug ? "console.log('on DOMContentLoaded');" : ''}
const imSize = resolveImgSize();
ipcRenderer.sendToHost('webview.img', imSize.width, imSize.height, 'dom-content-loaded');
if (
(img.play)
&& ((!img.paused) && (!img.ended) && (!(img.currentTime > 0)))
)
{ img.muted = true; img.loop = true; img.play(); }
});
document.addEventListener('load', (event) => {
${this.debug ? "console.log('on load');" : ''}
const imSize = resolveImgSize();
ipcRenderer.sendToHost('webview.img', imSize.width, imSize.height, 'load');
if (
(img.play)
&& ((!img.paused) && (!img.ended) && (!(img.currentTime > 0)))
)
{ img.muted = true; img.loop = true; img.play(); }
});
try {
if (img.play) { img.muted = true; img.loop = true; img.play(); }
} catch (err) {
console.error('Failed img.play()', err);
}
const updateSize = () => {
const imSize = resolveImgSize();
if ((imSize.width) && (imSize.height)) {
ipcRenderer.sendToHost('webview.img', imSize.width, imSize.height, 'updateSize');
} else {
setTimeout(() => updateSize(), 200);
}
}
updateSize();
let removeList = [];
const safeIds = ['flistWrapper', 'flistError', 'flistHider'];
const safeTags = [${_.map(safeTags, (t) => `'${t.toLowerCase()}'`).join(',')}];
body.childNodes.forEach((el) => (
(
(safeIds.indexOf(el.id) < 0)
&& ((!el.tagName) || (safeTags.indexOf(el.tagName.toLowerCase())) < 0)
) ? removeList.push(el) : true)
);
${skipElementRemove ? '' : 'removeList.forEach((el) => el.remove());'}
removeList = [];
`;
return js.replace(/\/\* ## SETTINGS_START[^]*SETTINGS_END ## \*\//m, `this.settings = ${settingsJson}`);
}
getErrorMutator(code: number, description: string): string {
const errorHtml = `
<div id="flistError" style="

View File

@ -23,7 +23,6 @@ const includedPaths = [
// 'spellchecker/build/Release/spellchecker.node',
'keytar/build/Release/keytar.node',
'throat',
'@cliqz/adblocker-electron',
'node-fetch',
'jquery'
];

View File

@ -25,7 +25,8 @@ const mainConfig = {
}
},
{test: path.join(__dirname, 'package.json'), loader: 'file-loader?name=package.json', type: 'javascript/auto'},
{test: /\.(png|html)$/, loader: 'file-loader?name=[name].[ext]'}
{test: /\.(png|html)$/, loader: 'file-loader?name=[name].[ext]'},
{test: /\.raw\.js$/, loader: 'raw-loader'}
]
},
node: {
@ -82,6 +83,7 @@ const mainConfig = {
{test: /\.(png|html)$/, loader: 'file-loader?name=[name].[ext]'},
{test: /\.vue\.scss/, loader: ['vue-style-loader','css-loader','sass-loader']},
{test: /\.vue\.css/, loader: ['vue-style-loader','css-loader']},
{test: /\.raw\.js$/, loader: 'raw-loader'}
]
},
node: {

4
index.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module "!!raw-loader!*" {
const content: string;
export default content;
}

View File

@ -29,6 +29,7 @@
"optimize-css-assets-webpack-plugin": "^5.0.1",
"qs": "^6.9.1",
"raven-js": "^3.27.2",
"raw-loader": "^4.0.0",
"sass-loader": "^7.1.0",
"sortablejs": "~1.9.0",
"style-loader": "^0.23.1",

View File

@ -5276,6 +5276,14 @@ raw-body@2.4.0:
iconv-lite "0.4.24"
unpipe "1.0.0"
raw-loader@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.0.tgz#d639c40fb9d72b5c7f8abc1fb2ddb25b29d3d540"
integrity sha512-iINUOYvl1cGEmfoaLjnZXt4bKfT2LJnZZib5N/LLyAphC+Dd11vNP9CNVb38j+SAJpFI1uo8j9frmih53ASy7Q==
dependencies:
loader-utils "^1.2.3"
schema-utils "^2.5.0"
rc@^1.2.7:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"