Spec-Zone.ru › Web Extensions

Обмен объектами со скриптами страницы

Примечание: Приёмы, описанные в этом разделе, доступны только в 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API