Я использую расширение Chrome с использованием MV3 для преобразования каждого кадра в теневой корень (чтобы встроить их содержимое). Мне это нужно, потому что я не могу копировать и вставлять iframe/embed/object
кадры из браузера в OneNote, Word и т. д., когда я выбираю все страницы, мне приходится выбирать содержимое фрейма одно за другим, но это отнимает много времени. Поэтому я решил конвертировать их, что позволяет мне скопировать их все сразу с помощью одного ctrl-c
.
Я решил iframes
, и преобразование работает очень хорошо, я использовал chrome.webNavigation.getAllFrames
для вставки каждого кадра и его родителя, затем я postMessage
между ними содержимое, а затем заменяю из родительского элемента содержимое подкадра, потому что теперь я могу идентифицировать подкадр по postMessage
frameid
У меня получилось getAllFrames
. но мне не удалось разгадать <embed>
кадры:
элементы (HTMLEmbedElement) не имеют contentWindow или contentDocument, поэтому мы не можем отправлять сообщение от родителя для его встраивания рамка. ссылка
Итак, чтобы получить содержимое, я использовал этот метод: window.frames[]
включает окна из <embed>
, но невозможно определить, принадлежит ли элемент window.frames[]
конкретному <embed>
, за исключением случая same-origin
, где frames[i].frameElement
можно сравнить с <embed>
элемент. ссылка
Но этот метод не всегда будет работать, потому что ShadowDOM
фреймы не отображаются в глобальном окне или фреймах, я имею в виду window.frames
не включать iframe/embed
, если они находятся внутри теневых корней. 1 2 3 4. И, конечно же, существует проблема перекрестного происхождения.
Другое решение — сравнить фрейм src
, который я получаю от getAllFrames
(или window.location.href
(отправленный из встроенного его родительскому элементу)) с frame.src
, который я получаю от родительского фрейма. Это позволяет мне определить по родительскому элементу правильный кадр внедрения, который мне нужно преобразовать в теневой корень. Но этот метод терпит неудачу, если кадр внедрения перенаправляется на новый URL-адрес, в стандарте это четко указано:
Атрибут src элемента не обновляется, если содержимое доступно для навигации получает дальнейшую навигацию в другие места. ссылка
Также есть runtime.getFrameId()
, который вроде бы решает все эти проблемы, но Chrome не хочет его реализовывать, Firefox его реализовал, но я использую chrome.
Одно из решений, которое я могу использовать, — это API webrequest
для перехвата перенаправлений и сохранения старых и новых URL-адресов для определения правильного фрейма, но это означает, что мне нужно обновить страницу, я хочу этого избежать, потому что я все это делаю работаю, чтобы заработать время, когда копирую/вставляю, чтобы не терять больше времени. Обновление означает, что мне нужно подождать больше времени и снова открыть все <details><summary>
... и т. д.
Я могу запустить Chrome с этими флагами --profile-directory = "%Profile_name%" --disable-site-isolation-trials --disable-web-security
это отключает веб-безопасность, что устраняет ограничения политики одного и того же происхождения, поэтому проблемы с перекрестным происхождением решаются, но я не могу все время работать с этими опасными флагами и каждый раз запускать новый Chrome с этими флагами только для копирования/вставки значит потерять больше времени...
Итак, теперь, чтобы идентифицировать кадр внедрения по его родительскому фрейму, я не могу использовать frame.src
от родительского фрейма, если он перенаправлен, и я не могу postmessage
от родительского к его дочернему фрейму внедрения, как я это делал в iframes
, и я не могу использовать window.frames
для cross-origin
фреймов или когда рамка находится внутри shadow-root
. Есть ли другое решение?
Если вам нужен тестовый пример, вот пример фрейма с перекрестным происхождением, который будет перенаправляться на новый URL-адрес.
<div><embed src = "https://iperasolutions.com" height = "500" width = "500"></div>
Это не сработало для кадров с разными источниками, но отлично работает для кадров с одним и тем же происхождением. по этому поводу есть открытая ошибка:
iframe перекрестного происхождения все еще не может быть отлажен с помощью chrome.debugger https://issues.chromium.org/issues/40752731
chrome.debugger.attach({ tabId: tabId }, "1.3", function () {
// Enable auto-attach to subtargets
chrome.debugger.sendCommand({ tabId: tabId }, "Target.setAutoAttach", {
autoAttach: true,
waitForDebuggerOnStart: false,
flatten: true,
}, function () {
chrome.debugger.sendCommand({ tabId: tabId }, "DOMSnapshot.captureSnapshot", { computedStyles: [] }, function (snapshot) {
console.info("snapshot", snapshot);
});
});
DOM.getDocument
не возвращает кадры перекрестного происхождения, а также команды Page.getFrameTree
и Page.getResourceTree
const getDocument = await chrome.debugger.sendCommand({ tabId }, "DOM.getDocument", { depth: -1, pierce: true });
console.info("++++++++getDocument",getDocument);
const getFrameTree = await chrome.debugger.sendCommand({ tabId }, "Page.getFrameTree", {});
console.info("++++++++getFrameTree: ", getFrameTree);
const getResourceTree = await chrome.debugger.sendCommand({ tabId }, "Page.getResourceTree", {});
console.info("++++++++getResourceTree: ", getResourceTree);
когда я запускаю это в мониторе протокола в devtools, кадры из разных источников тоже не возвращаются
{"command":"DOM.getDocument" ,"parameters":{"depth": -1, "pierce": true}}
если я отключу веб-безопасность, запустив Chrome с помощью --disable-site-isolation-trials
--disable-web-security
... все приведенные выше 3 команды вернут кадры с перекрестным происхождением.
вот полный код:
const contextMap = new Map();
try {
chrome.debugger.onEvent.addListener(handleDebuggerEvents);
await chrome.debugger.attach({ tabId: tabId }, "1.3");
// When you call Runtime.enable, it sends executionContextCreated events for all existing execution contexts. So you don’t have to worry about connecting late. https://github.com/ChromeDevTools/devtools-protocol/issues/72
await chrome.debugger.sendCommand({ tabId: tabId }, "Runtime.enable");
console.info("contextMap",contextMap);
await injectScriptIntoAllFrames(tabId);
await chrome.debugger.detach({ tabId: tabId });
} catch (error) {
console.error(error);
}
async function injectScriptIntoAllFrames(tabId) {
const getDocument = await chrome.debugger.sendCommand({ tabId }, "DOM.getDocument", { depth: -1, pierce: true });
if (getDocument) {
console.info("++++++++getDocument",getDocument);
await traverseDOM(tabId, getDocument.root);
}
}
async function traverseDOM(tabId, node) {
if (node.frameId && (node.nodeName === 'IFRAME' || node.nodeName === 'EMBED' || node.nodeName === 'OBJECT')) {
console.info("frameid",node.frameId);
// const contextId = contextMap.get(node.frameId).id;
// uniqueContextId: is An alternative way to specify the execution context to evaluate in. Compared to contextId that may be reused across processes, this is guaranteed to be system-unique, so it can be used to prevent accidental evaluation of the expression in context different than intended (e.g. as a result of navigation across process boundaries). This is mutually exclusive with `contextId`.EXPERiMENTAL
const uniqueContextId = contextMap.get(node.frameId).uniqueId;
console.info("=========uniqueContextId",uniqueContextId);
await injectScriptIntoFrame(tabId, node.frameId, uniqueContextId);
}
if (node.children) {
for (const child of node.children) {
await traverseDOM(tabId, child);
}
}
}
async function injectScriptIntoFrame(tabId, frameId, uniqueContextId) {
const evaluateResult = await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
expression: `(async () => {
console.info("111",window.badr);
await new Promise((resolve) => {setTimeout(resolve, 3000);});
return window.badr;
})()`
,awaitPromise: true
// ,contextId: contextId
,uniqueContextId: uniqueContextId
// ,silent: true
})
if (evaluateResult) {
if (evaluateResult.exceptionDetails) {
console.error(`An error happened or Failed to inject script into frame ${frameId}:`, evaluateResult.exceptionDetails);
} else {
console.info("Runtime.evaluate result",evaluateResult.result.value);
}
}
}
function handleDebuggerEvents (source, method, params) {
if (source.tabId !== tabId) return;
if (method === "Runtime.executionContextsCleared") {
contextMap.clear();
console.info("Contexts cleared");
}
if (method === "Runtime.executionContextCreated") {
const context = params.context;
const frameId = context.auxData && context.auxData.frameId;
// isDefault seems to be true only for main frame context,when i create a createIsolatedWorld or i inject a content script into the frame i see that isDefault is always false for those contexts. for main frame context the "name" seems to be always empty, and "type" is always type: 'default', type in general may be: 'default'|'isolated'|'worker'
if (frameId && context.auxData.isDefault) {
contextMap.set(frameId, context);
console.info(`Context saved: ${frameId}`, contextMap.get(frameId));
}
} else if (method === "Runtime.executionContextDestroyed") {
const executionContextId = params.executionContextId;
for (const [frameId, context] of contextMap) {
if (context.id === executionContextId) {
contextMap.delete(frameId);
console.info(`Context removed: ${frameId}`);
break;
}
}
}
}
это также не возвращает кадры перекрестного происхождения.
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const tabId = tabs[0].id;
chrome.debugger.attach({ tabId }, "1.3", () => {
chrome.debugger.sendCommand({ tabId }, "DOM.getDocument", {}, (result) => {
const rootNodeId = result.root.nodeId;
requestChildNodes(tabId, rootNodeId);
});
});
});
function requestChildNodes(tabId, nodeId) {
chrome.debugger.sendCommand({ tabId },"DOM.requestChildNodes",{ nodeId },(result) => {
console.info("Child nodes requested", result);
chrome.debugger.sendCommand({ tabId },"DOM.querySelectorAll",{ nodeId: nodeId, selector: '*' },(result) => {
console.info("All nodes:", result);
});
});
}
chrome.debugger.onEvent.addListener((source, method, params) => {
if (method === "DOM.setChildNodes") {
console.info("Child nodes set", params);
}
});
я нашел это, которое кажется похожим и применимым к приведенным выше командам, это примерно Page.getFrameTree
Метод CDP Page.getFrameTree с API chrome.debugger не возвращает междоменные iframe
Статус: не будет исправлено (предполагаемое поведение)
Page.getFrameTree возвращает только локальные фреймы, и он работает как положено. Чтобы прикрепить к внепроцессные iframe (которые являются отдельными целями CDP и на которых вы можете снова вызвать Page.getFrameTree, чтобы получить локальные деревья фреймов), в последнюю версию Canary, вы можете использовать автоматическое подключение CDP для автоматического создания сессии для всех OOPIF https://chromium-review.googlesource.com/c/chromium/src/+/5398119
так что, похоже, ему нужен как минимум Chrome v124+ и автоматическое подключение, так что у пользователей Win 7 и Chrome v109, таких как я, нет шансов :(
@wOxxOm, пожалуйста, прочтите мое обновление в конце части вопроса. это работало нормально для кадров одного и того же происхождения, но кадры перекрестного происхождения пусты. это кажется ошибкой, если я не ошибаюсь, посмотрите эту проблему: они говорят: iframe с перекрестным происхождением все еще не могут быть отлажены через chrome.debugger Issues.chromium.org/issues/40752731 кажется, что он работает нормально, когда мы используем CDP из внешних расширений Chrome. правильный ли мой приведенный выше код или я делаю это неправильно? любое предложение или другое решение, пожалуйста?
Почему бы вам не заменить эти элементы на iframe?
@Kaiido, какие элементы? мне нужно заменить только один элемент: <embed>
, и чтобы заменить его, мне нужно идентифицировать его по его родительскому элементу, но я не могу его идентифицировать, потому что мы не можем использовать postmessage
с элементами встраивания, а frame.src
также не работает, когда встраивание перенаправляется на новое место.
Я имею в виду, что каждый раз, когда <embed> добавляется в DOM, вы заменяете его <iframe>, чтобы в DOM вообще не было <embed>. В любом случае они не предоставляют ничего, кроме <iframe> (и в наши дни их использование не рекомендуется).
@Kaiido, да, но это означает, что мне придется перехватывать все сайты, на которые я просматриваю, с помощью веб-запроса, или обновлять страницу, чтобы перехватывать трафик, когда мне нужно скопировать/вставить, что требует больше времени и много ресурсов. я надеюсь найти лучшее решение
(Я не специалист по расширениям для браузеров), не можете ли вы внедрить код, который будет делать что-то вроде [...document.querySelectorAll("embed")].forEach(el => { const { src, width, height } = el; el.replaceWith(Object.assign(document.createElement("iframe"), { src, width, height }));});
?
@Kaiido вау просто вау, твое решение лишило меня дара речи с открытым ртом, это решило мою проблему, мне это нравится. если вы хотите оставить свой комментарий в качестве ответа, я приму его. большое спасибо за ваше время и пример кода.
С помощью chrome.debugger вы можете использовать тот же метод, который devtools использует для этих iframe, то есть DOM.getDocument, DOM.getFrameOwner, DOM.requestChildNodes. Вы можете увидеть это, включив монитор протокола в экспериментах с инструментами разработчика.
@woxxom извините за столь позднее подтверждение, но мне потребовалось все это время, чтобы изучить и проверить то, что вы мне сказали. к сожалению, ни одна из этих команд не сработала, кадры из разных источников не возвращаются, см. мое второе обновление выше под названием: обновление 2 для второго предложения wOxxOm. Удавалось ли вам когда-нибудь успешно использовать какую-либо из этих команд в расширении Chrome с фреймами из разных источников? если да, то что я делаю не так?
Вам нужно использовать DOM.getFrameOwner. Используйте монитор протокола при проверке iframe в инструментах разработчика. Чтобы сделать это быстрее, вы можете ввести DOM.
в поле ввода фильтра на панели инструментов.
@woxxom вот что произошло: когда я загружаю страницу в первый раз, отправляются DOM.getDocument
, DOM.getFrameOwner
и DOM.requestChildNodes
, а в ответах нет содержимого фрейма, как только я проверяю фрейм с помощью мыши, ни одна из этих трех команд не отправляется, и я получаю только событие DOM.setChildNodes
, содержащее содержимое фрейма. поэтому эти 3 команды никогда не отправляются для получения события setChildNodes
, поэтому мы не можем использовать их, как вы мне сказали, похоже, что именно щелчок вызывает setChildNodes
!! Есть идеи?
@woxxom я тестировал это <div><embed src = "https://api.ipify.org?format=jsonp&callback=getIP" height = "100" width = "100"></div>
Я думаю, он сломан, и я не настолько разбираюсь в chrome.debugger, чтобы найти обходной путь. Вы можете запустить Chrome с помощью командной строки --remote-debugging-port=1234
и написать внешний скрипт в node.js, который использует CDP через этот порт, и вызывать его из своего расширения, используя chrome.runtime.connectNative
.
@woxxom хорошо. да, использование CDP из внешнего расширения Chrome должно работать, я использовал его раньше, и у меня не было никаких проблем, оно настолько мощное, но для нашей проблемы я думаю, что внешний CDP будет слишком громоздким, поэтому я думаю, что мне придется остановитесь здесь, кажется, что для этого не существует 100% идеального решения (кроме внешнего CDP), но в любом случае под вашим руководством я узнал много вещей, которые более ценны, чем поиск решения, большое спасибо за ваше время и терпение.
Один из способов — заменить все элементы <embed>
на странице на <iframe>
, чтобы вы могли общаться с ней по своему усмотрению.
Должно подойти введение чего-то вроде ниже.
[...document.querySelectorAll("embed")].forEach((embed) => {
const { src, width, height } = embed;
const frame = Object.assign(document.createElement("iframe"), { src, width, height })
embed.replaceWith(frame);
});
(Это может привести к некоторым различиям в макете страницы).
это умное решение: преобразуйте его в iframe и позвольте iframe повторно загрузить src, это решит проблему перенаправления. просто и лаконично, я потратил много времени на поиск решения и никогда не думал об этом. Огромное спасибо
Это не удастся, если встраивание осуществляется через location.href = newUrl, который не изменяется src
в элементе. Он также сбросит состояние JS/DOM, хотя iframe с перекрестным происхождением обычно не имеет состояния.
@wOxxOm в случае location.href
он также выполнит его в iframe, так что это не проблема. Реальная проблема будет заключаться в том случае, если местоположение или DOM были изменены в результате взаимодействия пользователя, но поскольку в этом случае пользователь является OP, я полагаю, что это нормально.
Нет никакой гарантии, что это произойдет, если навигация происходит в ответ на сообщение, отправленное родительской страницей только один раз после создания встраивания. Я не думаю, что это частый сценарий, но это, безусловно, возможно.
Законный специалист по внедрению может сделать это через окно [0], проблема заключалась в том, что расширение не могло знать, какое окно [xxx] соответствует какому внедрению. Другая возможность заключается в том, что встраивание использует родительский.postMessage, а родительский элемент отвечает новыми данными, используемыми встраиванием для навигации, а родительский элемент мог использовать одноразовый прослушиватель, когда встраивание использовалось в первый раз, т. е. реакции не будет. когда он заменяется iframe.
@woxxom черт возьми, ты прав. Совершенно забыл, что там было раскрыто... Что касается одноразового обработчика сообщений... конечно, это возможно, хотя это звучит весьма маловероятно. Тем не менее, кажется, у вас есть лучшее решение? Пожалуйста, предоставьте это!
Не обязательно лучшее решение, но chrome.debugger может использоваться для того, что делает devtools для извлечения содержимого. Однако это сложно и при использовании отображается предупреждение в верхней части браузера.
@woxxom Тем не менее, поскольку это похоже на личное расширение, это может быть хорошей альтернативой. Кроме того, этот ответ с самого начала представляет собой вики-сообщество, поэтому не стесняйтесь добавлять любые предостережения из этого решения в качестве дополнения к нему.
Может быть, использовать API chrome.debugger и отправить DOMSnapshot.captureSnapshot?