Как идентифицировать или отправить сообщение в элемент <embed> с перекрестным происхождением после перенаправления

Я использую расширение Chrome с использованием MV3 для преобразования каждого кадра в теневой корень (чтобы встроить их содержимое). Мне это нужно, потому что я не могу копировать и вставлять iframe/embed/object кадры из браузера в OneNote, Word и т. д., когда я выбираю все страницы, мне приходится выбирать содержимое фрейма одно за другим, но это отнимает много времени. Поэтому я решил конвертировать их, что позволяет мне скопировать их все сразу с помощью одного ctrl-c.

неудачные решения:

сообщениеСообщение

Я решил iframes, и преобразование работает очень хорошо, я использовал chrome.webNavigation.getAllFrames для вставки каждого кадра и его родителя, затем я postMessage между ними содержимое, а затем заменяю из родительского элемента содержимое подкадра, потому что теперь я могу идентифицировать подкадр по postMessageframeid У меня получилось 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

Другое решение — сравнить фрейм src, который я получаю от getAllFrames (или window.location.href (отправленный из встроенного его родительскому элементу)) с frame.src, который я получаю от родительского фрейма. Это позволяет мне определить по родительскому элементу правильный кадр внедрения, который мне нужно преобразовать в теневой корень. Но этот метод терпит неудачу, если кадр внедрения перенаправляется на новый URL-адрес, в стандарте это четко указано:

Атрибут src элемента не обновляется, если содержимое доступно для навигации получает дальнейшую навигацию в другие места. ссылка

getFrameId()

Также есть 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>

обновление для предложения wOxxOm

Это не сработало для кадров с разными источниками, но отлично работает для кадров с одним и тем же происхождением. по этому поводу есть открытая ошибка:

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);
    });
});

обновление 2 для второго предложения wOxxOm

DOM.getDocument

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;
      }
    }
  }
}

DOM.requestChildNodes

это также не возвращает кадры перекрестного происхождения.

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);
  }
});

возможное исправление для Chrome v124+ (не проверялось!)

я нашел это, которое кажется похожим и применимым к приведенным выше командам, это примерно 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

источник: https://issues.chromium.org/issues/333981074

так что, похоже, ему нужен как минимум Chrome v124+ и автоматическое подключение, так что у пользователей Win 7 и Chrome v109, таких как я, нет шансов :(

Может быть, использовать API chrome.debugger и отправить DOMSnapshot.captureSnapshot?

woxxom 22.05.2024 16:53

@wOxxOm, пожалуйста, прочтите мое обновление в конце части вопроса. это работало нормально для кадров одного и того же происхождения, но кадры перекрестного происхождения пусты. это кажется ошибкой, если я не ошибаюсь, посмотрите эту проблему: они говорят: iframe с перекрестным происхождением все еще не могут быть отлажены через chrome.debugger Issues.chromium.org/issues/40752731 кажется, что он работает нормально, когда мы используем CDP из внешних расширений Chrome. правильный ли мой приведенный выше код или я делаю это неправильно? любое предложение или другое решение, пожалуйста?

Badr Elmers 23.05.2024 01:42

Почему бы вам не заменить эти элементы на iframe?

Kaiido 23.05.2024 01:45

@Kaiido, какие элементы? мне нужно заменить только один элемент: <embed>, и чтобы заменить его, мне нужно идентифицировать его по его родительскому элементу, но я не могу его идентифицировать, потому что мы не можем использовать postmessage с элементами встраивания, а frame.src также не работает, когда встраивание перенаправляется на новое место.

Badr Elmers 23.05.2024 01:50

Я имею в виду, что каждый раз, когда <embed> добавляется в DOM, вы заменяете его <iframe>, чтобы в DOM вообще не было <embed>. В любом случае они не предоставляют ничего, кроме <iframe> (и в наши дни их использование не рекомендуется).

Kaiido 23.05.2024 02:13

@Kaiido, да, но это означает, что мне придется перехватывать все сайты, на которые я просматриваю, с помощью веб-запроса, или обновлять страницу, чтобы перехватывать трафик, когда мне нужно скопировать/вставить, что требует больше времени и много ресурсов. я надеюсь найти лучшее решение

Badr Elmers 23.05.2024 02:32

(Я не специалист по расширениям для браузеров), не можете ли вы внедрить код, который будет делать что-то вроде [...document.querySelectorAll("embed")].forEach(el => { const { src, width, height } = el; el.replaceWith(Object.assign(document.createElement("iframe"‌​), { src, width, height }));});?

Kaiido 23.05.2024 02:45

@Kaiido вау просто вау, твое решение лишило меня дара речи с открытым ртом, это решило мою проблему, мне это нравится. если вы хотите оставить свой комментарий в качестве ответа, я приму его. большое спасибо за ваше время и пример кода.

Badr Elmers 23.05.2024 03:03

С помощью chrome.debugger вы можете использовать тот же метод, который devtools использует для этих iframe, то есть DOM.getDocument, DOM.getFrameOwner, DOM.requestChildNodes. Вы можете увидеть это, включив монитор протокола в экспериментах с инструментами разработчика.

woxxom 23.05.2024 07:39

@woxxom извините за столь позднее подтверждение, но мне потребовалось все это время, чтобы изучить и проверить то, что вы мне сказали. к сожалению, ни одна из этих команд не сработала, кадры из разных источников не возвращаются, см. мое второе обновление выше под названием: обновление 2 для второго предложения wOxxOm. Удавалось ли вам когда-нибудь успешно использовать какую-либо из этих команд в расширении Chrome с фреймами из разных источников? если да, то что я делаю не так?

Badr Elmers 24.05.2024 18:47

Вам нужно использовать DOM.getFrameOwner. Используйте монитор протокола при проверке iframe в инструментах разработчика. Чтобы сделать это быстрее, вы можете ввести DOM. в поле ввода фильтра на панели инструментов.

woxxom 24.05.2024 19:00

@woxxom вот что произошло: когда я загружаю страницу в первый раз, отправляются DOM.getDocument, DOM.getFrameOwner и DOM.requestChildNodes, а в ответах нет содержимого фрейма, как только я проверяю фрейм с помощью мыши, ни одна из этих трех команд не отправляется, и я получаю только событие DOM.setChildNodes, содержащее содержимое фрейма. поэтому эти 3 команды никогда не отправляются для получения события setChildNodes, поэтому мы не можем использовать их, как вы мне сказали, похоже, что именно щелчок вызывает setChildNodes!! Есть идеи?

Badr Elmers 24.05.2024 20:57

@woxxom я тестировал это <div><embed src = "https://api.ipify.org?format=jsonp&callback=getIP" height = "100" width = "100"></div>

Badr Elmers 24.05.2024 20:59

Я думаю, он сломан, и я не настолько разбираюсь в chrome.debugger, чтобы найти обходной путь. Вы можете запустить Chrome с помощью командной строки --remote-debugging-port=1234 и написать внешний скрипт в node.js, который использует CDP через этот порт, и вызывать его из своего расширения, используя chrome.runtime.connectNative.

woxxom 24.05.2024 21:46

@woxxom хорошо. да, использование CDP из внешнего расширения Chrome должно работать, я использовал его раньше, и у меня не было никаких проблем, оно настолько мощное, но для нашей проблемы я думаю, что внешний CDP будет слишком громоздким, поэтому я думаю, что мне придется остановитесь здесь, кажется, что для этого не существует 100% идеального решения (кроме внешнего CDP), но в любом случае под вашим руководством я узнал много вещей, которые более ценны, чем поиск решения, большое спасибо за ваше время и терпение.

Badr Elmers 24.05.2024 22:15
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
0
15
80
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Один из способов — заменить все элементы <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, это решит проблему перенаправления. просто и лаконично, я потратил много времени на поиск решения и никогда не думал об этом. Огромное спасибо

Badr Elmers 23.05.2024 03:12

Это не удастся, если встраивание осуществляется через location.href = newUrl, который не изменяется src в элементе. Он также сбросит состояние JS/DOM, хотя iframe с перекрестным происхождением обычно не имеет состояния.

woxxom 23.05.2024 07:33

@wOxxOm в случае location.href он также выполнит его в iframe, так что это не проблема. Реальная проблема будет заключаться в том случае, если местоположение или DOM были изменены в результате взаимодействия пользователя, но поскольку в этом случае пользователь является OP, я полагаю, что это нормально.

Kaiido 23.05.2024 07:36

Нет никакой гарантии, что это произойдет, если навигация происходит в ответ на сообщение, отправленное родительской страницей только один раз после создания встраивания. Я не думаю, что это частый сценарий, но это, безусловно, возможно.

woxxom 23.05.2024 08:27

Законный специалист по внедрению может сделать это через окно [0], проблема заключалась в том, что расширение не могло знать, какое окно [xxx] соответствует какому внедрению. Другая возможность заключается в том, что встраивание использует родительский.postMessage, а родительский элемент отвечает новыми данными, используемыми встраиванием для навигации, а родительский элемент мог использовать одноразовый прослушиватель, когда встраивание использовалось в первый раз, т. е. реакции не будет. когда он заменяется iframe.

woxxom 23.05.2024 11:03

@woxxom черт возьми, ты прав. Совершенно забыл, что там было раскрыто... Что касается одноразового обработчика сообщений... конечно, это возможно, хотя это звучит весьма маловероятно. Тем не менее, кажется, у вас есть лучшее решение? Пожалуйста, предоставьте это!

Kaiido 23.05.2024 13:19

Не обязательно лучшее решение, но chrome.debugger может использоваться для того, что делает devtools для извлечения содержимого. Однако это сложно и при использовании отображается предупреждение в верхней части браузера.

woxxom 23.05.2024 14:47

@woxxom Тем не менее, поскольку это похоже на личное расширение, это может быть хорошей альтернативой. Кроме того, этот ответ с самого начала представляет собой вики-сообщество, поэтому не стесняйтесь добавлять любые предостережения из этого решения в качестве дополнения к нему.

Kaiido 23.05.2024 16:15

Другие вопросы по теме