From 12acc5b99a3ca434bc53d97977a4befd46975486 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 30 Jan 2024 20:13:52 +0100 Subject: [PATCH] Fix local iframes (#112) * Fix for local iframes * fix replace html function for contenteditable * reconfigure webpack * Add name to rich text editors list * Fix wordpress --- manifest.json | 2 +- package.json | 2 +- src/constants.ts | 7 ++- src/elements.ts | 97 ++++++++++++++++++++++++++++++++------- src/searchreplace.ts | 107 ++++++++++++++++++++++--------------------- 5 files changed, 142 insertions(+), 73 deletions(-) diff --git a/manifest.json b/manifest.json index aa49e0f..f8fef12 100644 --- a/manifest.json +++ b/manifest.json @@ -31,7 +31,7 @@ "permissions": ["activeTab", "storage", "notifications"], "host_permissions": ["http://*/*", "https://*/*"], "update_url": "http://clients2.google.com/service/update2/crx", - "version":"2.0.7", + "version":"2.0.8", "options_page": "assets/options.html", "icons": { "16": "assets/icon-16.png", diff --git a/package.json b/package.json index 33c6dfe..fbddf85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "search_and_replace", - "version": "2.0.7", + "version": "2.0.8", "resolutions": { "author": "Chris Taylor " }, diff --git a/src/constants.ts b/src/constants.ts index d46ad54..3283440 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,7 +1,10 @@ import { Hint } from './types' - +export const RICH_TEXT_EDITORS = { + class: ['mce-edit-area', 'cke_wysiwyg_frame', 'tinymce', 'wysiwyg'], + name: ['editor-canvas'], +} export const ELEMENT_FILTER = /(HTML|HEAD|SCRIPT|STYLE|IFRAME)/i -export const INPUT_TEXTAREA_FILTER = /(INPUT|TEXTAREA)/ +export const INPUT_TEXTAREA_CONTENT_EDITABLE_SELECTOR = 'input,textarea,*[contenteditable="true"]' export const HINTS: Record = { gmail: { hint: 'Hint: Gmail detected. Check "Input fields only?" when editing draft emails.', diff --git a/src/elements.ts b/src/elements.ts index 42fd5fc..e621f65 100644 --- a/src/elements.ts +++ b/src/elements.ts @@ -1,50 +1,113 @@ // Utils for dealing with Elements -import { HINTS } from './constants' +import { HINTS, INPUT_TEXTAREA_CONTENT_EDITABLE_SELECTOR, RICH_TEXT_EDITORS } from './constants' import { SearchReplaceResult } from './types' +import { notEmpty } from './util' export function getInputElements( - document: Document, + document: HTMLElement | Document, elementFilter: Map, hiddenContent?: boolean ): (HTMLInputElement | HTMLTextAreaElement)[] { const inputs = Array.from( - >document.querySelectorAll('input,textarea,div[contenteditable="true"]') + >document.querySelectorAll(INPUT_TEXTAREA_CONTENT_EDITABLE_SELECTOR) ) const visibleElements = !hiddenContent ? inputs.filter((input) => elementIsVisible(input, true, false)) : inputs return visibleElements.filter((input) => !elementFilter.has(input)) } +export function isInputElement(el: Element): el is HTMLInputElement { + return ( + el.tagName === 'INPUT' || + el.tagName === 'TEXTAREA' || + (el.hasAttribute('contentEditable') && el.getAttribute('contentEditable') === 'true') + ) +} + export function isBlobIframe(el: Element) { return el.tagName === 'IFRAME' && 'src' in el && typeof el.src === 'string' && el.src.startsWith('blob:') } -export function getIframeElements(document: Document, blob = false): HTMLIFrameElement[] { +export function isWYSIWYGEditorIframe(el: Element) { + return ( + RICH_TEXT_EDITORS.name.some((editor) => 'name' in el && el.name === editor) || + RICH_TEXT_EDITORS.class.some((editor) => containsPartialClass(el, editor)) + ) +} + +export function containsPartialClass(element: Element, partialClass: string) { + return Array.from(element.classList).some((c) => c.includes(partialClass)) +} + +export function getLocalIframes(window: Window, document: Document): HTMLIFrameElement[] { + return Array.from(>document.querySelectorAll('iframe')).filter((iframe) => { + return iframe.src === '' || iframe.src === 'about:blank' || iframe.src === window.location.href + }) +} + +export function getWYSIWYGEditorIframes(window: Window, document: Document): HTMLIFrameElement[] { + return getLocalIframes(window, document).filter((iframe) => { + return isWYSIWYGEditorIframe(iframe) + }) +} + +export function getBlobIframes(document: Document): HTMLIFrameElement[] { + const blobIframes = Array.from(>document.querySelectorAll('iframe')).filter( + (iframe) => { + return isBlobIframe(iframe) + } + ) + return blobIframes +} + +export const waitForIframeLoad = async (iframe: HTMLIFrameElement): Promise => { + return new Promise((resolve, reject) => { + if (iframe.contentDocument) { + if (iframe.contentDocument.readyState !== 'complete') { + iframe.contentDocument.onreadystatechange = () => { + if (iframe.contentDocument && iframe.contentDocument.readyState === 'complete') { + resolve(iframe) + } + } + } else { + resolve(iframe) + } + } else { + resolve(null) + } + }) +} + +export async function getSearchableIframes(window: Window, document: Document): Promise { + return ( + await Promise.all( + [...getBlobIframes(document), ...getWYSIWYGEditorIframes(window, document)].map((iframe) => { + return waitForIframeLoad(iframe) + }) + ) + ).filter(notEmpty) +} + +export function getRespondingIframes(window: Window, document: Document): HTMLIFrameElement[] { // we don't want to count iframes in gmail if (document.querySelector(HINTS['gmail'].selector) || window.location.href.indexOf(HINTS['gmail'].domain) > -1) { return [] } return Array.from(>document.querySelectorAll('iframe')).filter((iframe) => { - console.log('iframe.src.length', iframe.src.length) - console.log("iframe.src.startsWith('chrome-extension://')", iframe.src.startsWith('chrome-extension://')) - console.log('window location origin', window.location.origin) - console.log('iframe.src.startsWith(window.location.origin)', iframe.src.startsWith(window.location.origin)) - console.log('blob', blob) - console.log('isBlobIframe(iframe)', isBlobIframe(iframe)) - console.log( - '(blob ? isBlobIframe(iframe) : !isBlobIframe(iframe))', - blob ? isBlobIframe(iframe) : !isBlobIframe(iframe) - ) - // We don't want empty iframes const want = + // loaded iframes containing a content script need to satisfy the following conditions + iframe.src !== undefined && + // we don't want WYSIWYG editors that use iframes + !getWYSIWYGEditorIframes(window, document).includes(iframe) && + // we don't want iframes with no src iframe.src.length > 0 && // We don't want to count iframes injected by other chrome extensions !iframe.src.startsWith('chrome-extension://') && // We only want to wait on iframes from the same origin OR blob iframes iframe.src.indexOf(window.location.origin) > -1 && - // We may or may not want blob iframes - (blob ? isBlobIframe(iframe) : !isBlobIframe(iframe)) + // We do not want blob iframes + !isBlobIframe(iframe) return want }) } diff --git a/src/searchreplace.ts b/src/searchreplace.ts index c3a01bb..ef1ac60 100644 --- a/src/searchreplace.ts +++ b/src/searchreplace.ts @@ -11,12 +11,15 @@ import { import { copyElementAndRemoveSelectedElements, elementIsVisible, - getIframeElements, getInitialIframeElement, getInputElements, + getRespondingIframes, + getSearchableIframes, inIframe, isBlobIframe, isHidden, + isInputElement, + isWYSIWYGEditorIframe, } from './elements' import { getFlags, getSearchPattern } from './regex' import { getHints } from './hints' @@ -59,18 +62,13 @@ function setNativeValue(element: HTMLInputElement | HTMLTextAreaElement, value: prototypeValueSetter = prototypeValueFn.set } if (valueSetter && prototypeValueSetter && valueSetter !== prototypeValueSetter) { - console.log('prototypeValueSetter.call(element, value)') prototypeValueSetter.call(element, value) } else if (valueSetter) { valueSetter.call(element, value) - console.log('valueSetter.call(element, value)') } else { element.value = value element.setAttribute('value', value) element.shadowRoot?.getElementById(element.id)?.setAttribute('value', value) - console.log(element) - console.log('element.value = value') - console.log(element) } } @@ -105,23 +103,28 @@ function replaceInInputShadow( function getValue(node: Element | Node, config: SearchReplaceConfig): string { const nodeElement = getElementFromNode(node) + console.log('nodeElement', nodeElement) // if it's an input or a textarea, take the value if (nodeElement && (nodeElement.nodeName === 'INPUT' || nodeElement.nodeName === 'TEXTAREA')) { return nodeElement['value'] } - // if it's a contenteditable div, take the innerHTML - if (nodeElement && nodeElement.nodeName === 'DIV' && nodeElement.getAttribute('contenteditable') === 'true') { - return nodeElement['innerHTML'] + // if it's a contenteditable div, take the outerHTML if we're replacing HTML, otherwise take the innerHTML + if (nodeElement && nodeElement.nodeName.match(/DIV|BODY/g) && nodeElement.hasAttribute('contenteditable')) { + console.log('returning outer / innerHTML') + return config.searchTarget === 'innerHTML' ? nodeElement['outerHTML'] : nodeElement['innerHTML'] } // if the search target is innerHTML, take the innerHTML if (nodeElement && config.searchTarget === 'innerHTML') { + console.log('returning innerHTML') return nodeElement['innerHTML'] } // If it's a text node, return the nodeValue if (node.nodeType === Node.TEXT_NODE) { + console.log('returning nodeValue') return node.nodeValue || '' } // Otherwise return the innerText + console.log('returning innerText') return node['innerText'] } @@ -184,14 +187,14 @@ function containsAncestor(element: Element, results: Map