Обмен объектами со скриптами страницы
Примечание: Приёмы, описанные в этом разделе, доступны только в Firefox, начиная с версии 49.
Предупреждение: Разработчик расширения должен понимать, что скрипты, работающие в произвольных веб-страницах, являются потенциально вредоносными, цель которых — кража личной информации пользователя, повреждение его компьютера или нанесение другого вреда.
Изоляция между скриптами содержимого и скриптами, загруженными веб-страницами, предназначена для затруднения подобных действий со стороны вредоносных веб-страниц.
Поскольку описанные в этом разделе приёмы нарушают эту изоляцию, они потенциально опасны и должны использоваться с большой осторожностью.
Как отмечается в руководстве по скриптам содержимого, скрипты содержимого не видят изменений, вносимых в DOM скриптами, загруженными веб-страницами. Это означает, что, например, если веб-страница загружает библиотеку, такую как jQuery, скрипты содержимого не смогут её использовать и должны загрузить свою собственную копию. И наоборот, скрипты, загруженные веб-страницами, не могут видеть изменений, внесённых скриптами содержимого.
Однако Firefox предоставляет некоторые API, которые позволяют скриптам содержимого:
- доступ к объектам JavaScript, созданным скриптами страницы
- экспортировать свои собственные объекты JavaScript для использования скриптами страницы.
Видение сквозь страницы в Firefox
В Firefox часть изоляции между скриптами содержимого и скриптами страницы реализована с помощью функции «видения сквозь страницы». Когда скрипт с более высокими привилегиями обращается к объекту, определённому в скрипте с меньшими привилегиями, он видит только «родную версию» объекта. Любые свойства-расширения (expando) не видны, и если какие-либо свойства объекта были переопределены, скрипт видит исходную реализацию, а не переопределённую.
Цель этой функции — затруднить для скрипта с меньшими привилегиями введение в заблуждение скрипта с более высокими привилегиями путём переопределения родных свойств объектов.
Например, когда скрипт содержимого обращается к глобальному объекту window страницы, он не увидит свойства, добавленные скриптом страницы к объекту window, и если скрипт страницы переопределил какие-либо существующие свойства объекта window, скрипт содержимого увидит исходную версию.
Доступ к объектам скриптов страницы из скриптов содержимого
В Firefox DOM-объекты в скриптах содержимого получают дополнительное свойство wrappedJSObject. Это «распакованная» версия объекта, которая включает все изменения, внесённые в этот объект скриптами страницы.
Рассмотрим простой пример. Предположим, что веб-страница загружает скрипт:
<!DOCTYPE html> <html lang="en-US"> <head> <meta charset="UTF-8" /> </head> <body> <script src="main.js"></script> </body> </html>
Скрипт добавляет свойство-расширение к глобальному объекту window.
// main.js let foo = "I'm defined in a page script!";
Функция «видения сквозь страницы» означает, что если скрипт содержимого пытается получить доступ к foo, он будет undefined:
// content-script.js console.log(window.foo); // undefined
В Firefox скрипты содержимого могут использовать window.wrappedJSObject для просмотра свойства-расширения:
// content-script.js console.log(window.wrappedJSObject.foo); // "I'm defined in a page script!"
Обратите внимание, что после этого вы больше не можете полагаться на то, что свойства или функции этого объекта будут соответствовать вашим ожиданиям. Любое из них, включая сеттеры и геттеры, может быть переопределено небезопасным кодом.
Также обратите внимание, что распаковывание транзитивно: когда вы используете wrappedJSObject, любые свойства распакованного объекта также распаковываются (и поэтому ненадежны). Поэтому рекомендуется после получения нужного объекта переупаковать его, что можно сделать так:
XPCNativeWrapper(window.wrappedJSObject.foo);
Более подробную информацию об этой функции вы найдёте в документе по видению сквозь страницы.
Обмен объектами скриптов содержимого со скриптами страницы
Firefox также предоставляет API, позволяющие скриптам содержимого предоставлять доступ к объектам для скриптов страницы. Существует несколько подходов:
-
exportFunction(): экспорт функции для скриптов страницы. -
cloneInto(): экспорт объекта для скриптов страницы. - конструкторы из контекста страницы
exportFunction
Указанная в скрипте содержимого функция exportFunction() экспортируется в область видимости скрипта страницы, чтобы скрипт страницы мог её вызвать.
Например, рассмотрим расширение с фоновым скриптом такого вида:
/* Execute content script in the active tab. */ function loadContentScript() { browser.tabs.executeScript({ file: "/content_scripts/export.js" }); } /* Add loadContentScript() as a listener to clicks on the browser action. */ browser.browserAction.onClicked.addListener(loadContentScript); /* Show a notification when we get messages from the content script. */ browser.runtime.onMessage.addListener((message) => { browser.notifications.create({ type: "basic", title: "Message from the page", message: message.content }); });
Это выполняет два действия:
- исполнение скрипта содержимого в текущей вкладке при нажатии пользователем действия браузера
- прослушивание сообщений от скрипта содержимого и отображение уведомления при поступлении сообщения.
Скрипт содержимого выглядит так:
/* Define a function in the content script's scope, then export it into the page script's scope. */ function notify(message) { browser.runtime.sendMessage({ content: `Function call: ${message}` }); } exportFunction(notify, window, { defineAs: "notify" });
Это определяет функцию notify(), которая просто отправляет свой аргумент в фоновый скрипт. Затем функция экспортируется в область видимости скрипта страницы. Теперь скрипт страницы может вызвать эту функцию:
window.notify("Message from the page script!");
cloneInto
Указанный в скрипте содержимого объект дублируется в области видимости скрипта страницы, делая дубликат доступным скриптам страницы. По умолчанию используется алгоритм структурированного клонирования structured clone algorithm, который не включает функции объекта в дубликат. Чтобы включить функции, передайте опцию cloneFunctions.
Например, вот скрипт содержимого, определяющий объект с функцией, затем клонирующий его в область видимости скрипта страницы:
/* Create an object that contains functions in the content script's scope, then clone it into the page script's scope. Because the object contains functions, the cloneInto call must include the `cloneFunctions` option. */ let messenger = { notify(message) { browser.runtime.sendMessage({ content: `Object method call: ${message}` }); } }; window.wrappedJSObject.messenger = cloneInto( messenger, window, {cloneFunctions: true});
Теперь скрипты страницы видят новое свойство в объекте window, messenger, содержащее функцию notify():
window.messenger.notify("Message from the page script!");
Конструкторы из контекста страницы
В объекте window, просмотренном с помощью функции «видения сквозь страницы», доступны исходные конструкторы некоторых встроенных объектов JavaScript, таких как Object, Function или Proxy, а также различные классы DOM. XMLHttpRequest не работает таким образом. Подробнее см. раздел XHR и fetch. Эти конструкторы создадут экземпляры, принадлежащие иерархии глобальных объектов страницы, а затем вернут оберточную оболочку.
Поскольку объекты, созданные таким способом, уже принадлежат странице, а не скрипту содержимого, их передача обратно на страницу не требует дополнительного клонирования или экспорта.
/* JavaScript built-ins */ const objA = new Object(); const objB = new window.Object(); console.log( objA instanceof Object, // true objB instanceof Object, // false objA instanceof window.Object, // false objB instanceof window.Object, // true 'wrappedJSObject' in objB // true; xrayed ); objA.foo = "foo"; objB.foo = "foo"; // xray wrappers for plain JavaScript objects pass through property assignments objB.wrappedJSObject.bar = "bar"; // unwrapping before assignment does not rely on this special behavior window.wrappedJSObject.objA = objA; window.wrappedJSObject.objB = objB; // automatically unwraps when passed to page context window.eval(` console.log(objA instanceof Object); // false console.log(objB instanceof Object); // true try { console.log(objA.foo); } catch (error) { console.log(error); // Error: permission denied } try { objA.baz = "baz"; } catch (error) { console.log(error); // Error: permission denied } console.log(objB.foo, objB.bar); // "foo", "bar" objB.baz = "baz"; `); /* other APIs */ const ev = new Event("click"); console.log( ev instanceof Event, // true ev instanceof window.Event, // true; Event constructor is actually inherited from the xrayed window 'wrappedJSObject' in ev // true; is an xrayed object ); ev.propA = "propA" // xray wrappers for native objects do not pass through assignments ev.propB = "wrapper"; // define property on xray wrapper ev.wrappedJSObject.propB = "unwrapped"; // define same property on page object Reflect.defineProperty(ev.wrappedJSObject, // privileged reflection can operate on less privileged objects 'propC', { get: exportFunction(() => { // getters must be exported like regular functions return 'propC'; }, window) } ); window.eval(` document.addEventListener("click", (e) => { console.log(e instanceof Event, e.propA, e.propB, e.propC); }); `); document.dispatchEvent(ev); // true, undefined, "unwrapped", "propC"
Клонирование Promise
Promise нельзя клонировать напрямую с помощью cloneInto, так как Promise не поддерживается алгоритмом структурированного клонирования. Однако желаемый результат можно получить, используя window.Promise вместо Promise, а затем клонируя значение разрешения следующим образом:
const promise = new window.Promise((resolve) => { // if just a primitive, then cloneInto is not needed: // resolve("string is a primitive"); // if not a primitive, such as an object, then the value must be cloned const result = { exampleKey: "exampleValue" }; resolve(cloneInto(result, window)); }); // now the promise can be passed to the web page
© 2005–2023 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Sharing_objects_with_page_scripts