From c7b8b53f9ac818f07dca8faf4eb55a2d04012dd8 Mon Sep 17 00:00:00 2001 From: "Mr. Stallion" Date: Sun, 5 Apr 2020 13:29:43 -0500 Subject: [PATCH] DOM mutator cleanup --- chat/preview/ImagePreview.vue | 9 +- chat/preview/assets/browser.pre.js | 18 +- chat/preview/assets/browser.processor.raw.js | 365 ++++++++++++++++++ ...review-mutator.ts => image-dom-mutator.ts} | 267 ++----------- electron/pack.js | 1 - electron/webpack.config.js | 4 +- index.d.ts | 4 + package.json | 1 + yarn.lock | 8 + 9 files changed, 441 insertions(+), 236 deletions(-) create mode 100644 chat/preview/assets/browser.processor.raw.js rename chat/preview/{image-preview-mutator.ts => image-dom-mutator.ts} (53%) create mode 100644 index.d.ts diff --git a/chat/preview/ImagePreview.vue b/chat/preview/ImagePreview.vue index 1146342..72084df 100644 --- a/chat/preview/ImagePreview.vue +++ b/chat/preview/ImagePreview.vue @@ -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 = {}; localPreviewStyle: Record = {}; @@ -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(); diff --git a/chat/preview/assets/browser.pre.js b/chat/preview/assets/browser.pre.js index 390ea31..edaafa8 100644 --- a/chat/preview/assets/browser.pre.js +++ b/chat/preview/assets/browser.pre.js @@ -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');"; diff --git a/chat/preview/assets/browser.processor.raw.js b/chat/preview/assets/browser.processor.raw.js new file mode 100644 index 0000000..708ed6d --- /dev/null +++ b/chat/preview/assets/browser.processor.raw.js @@ -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 ## */ diff --git a/chat/preview/image-preview-mutator.ts b/chat/preview/image-dom-mutator.ts similarity index 53% rename from chat/preview/image-preview-mutator.ts rename to chat/preview/image-dom-mutator.ts index f422ae8..378612f 100644 --- a/chat/preview/image-preview-mutator.ts +++ b/chat/preview/image-dom-mutator.ts @@ -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 = {}; + 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 { + this.scripts = { + processor: processorScript + }; + } + + + async init(): Promise { + 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 = `