Скрипты содержимого — это файлы, которые выполняются в контексте веб-страниц. Используя стандартную объектную модель документа (DOM), они могут считывать информацию о веб-страницах, которые посещает браузер, вносить в них изменения и передавать информацию своему родительскому расширению.
Понимание возможностей скриптов контента
Скрипты контента могут напрямую обращаться к следующим API расширений:
-
dom -
i18n -
storage -
runtime.connect() -
runtime.getManifest() -
runtime.getURL() -
runtime.id -
runtime.onConnect -
runtime.onMessage -
runtime.sendMessage()
Скрипты контента не могут напрямую обращаться к другим API. Но они могут обращаться к ним косвенно, обмениваясь сообщениями с другими частями вашего расширения.
Вы также можете получить доступ к другим файлам вашего расширения из скрипта контента, используя такие API, как fetch() . Для этого вам необходимо объявить их как веб-доступные ресурсы . Обратите внимание, что это также делает эти ресурсы доступными для любых собственных или сторонних скриптов, работающих на том же сайте.
Работа в изолированных мирах
Скрипты контента работают в изолированной среде, что позволяет им вносить изменения в свою среду JavaScript без конфликтов со скриптами контента страницы или других расширений.
Расширение может работать на веб-странице с кодом, аналогичным приведенному ниже примеру.
webPage.html
<html>
<button id="mybutton">click me</button>
<script>
var greeting = "hello, ";
var button = document.getElementById("mybutton");
button.person_name = "Bob";
button.addEventListener(
"click", () => alert(greeting + button.person_name + "."), false);
</script>
</html>
Это расширение может внедрить следующий скрипт содержимого, используя один из методов, описанных в разделе «Внедрение скриптов» .
content-script.js
var greeting = "hola, ";
var button = document.getElementById("mybutton");
button.person_name = "Roberto";
button.addEventListener(
"click", () => alert(greeting + button.person_name + "."), false);
Благодаря этому изменению, при нажатии кнопки оба предупреждения отображаются последовательно.
Внедрение скриптов
Скрипты контента могут быть объявлены статически , динамически или внедрены программно .
Внедрение с помощью статических объявлений
Используйте объявления статических скриптов в файле manifest.json для скриптов, которые должны автоматически запускаться на известном наборе страниц.
Статически объявленные скрипты регистрируются в манифесте под ключом "content_scripts" . Они могут включать файлы JavaScript, файлы CSS или и то, и другое. Все автоматически запускаемые скрипты содержимого должны указывать шаблоны соответствия .
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"css": ["my-styles.css"],
"js": ["content-script.js"]
}
],
...
}
| Имя | Тип | Описание |
|---|---|---|
matches | массив строк | Обязательный параметр. Указывает, на какие страницы будет внедрен этот скрипт содержимого. См. раздел «Шаблоны соответствия» для получения подробной информации о синтаксисе этих строк, а также раздел «Шаблоны соответствия и шаблоны» для получения информации о том, как исключить URL-адреса. |
css | массив строк | Необязательный параметр. Список CSS-файлов, которые будут внедрены в соответствующие страницы. Они внедряются в том порядке, в котором они указаны в этом массиве, до того, как будет создан или отображен какой-либо DOM-элемент для страницы. |
js | | Необязательный параметр. Список JavaScript-файлов, которые будут внедрены в соответствующие страницы. Файлы внедряются в том порядке, в котором они указаны в этом массиве. Каждая строка в этом списке должна содержать относительный путь к ресурсу в корневом каталоге расширения. Начальные косые черты (`/`) автоматически удаляются. |
run_at | RunAt | Необязательный параметр. Указывает, когда скрипт должен быть внедрен на страницу. По умолчанию используется document_idle . |
match_about_blank | логический | Необязательный параметр. Указывает, следует ли внедрять скрипт в фрейм about:blank если родительский или открывающий фрейм соответствует одному из шаблонов, указанных в matches . По умолчанию — false. |
match_origin_as_fallback | логический | Необязательно. Указывает, следует ли скрипту внедрять скрипт в фреймы, созданные соответствующим источником, но чей URL-адрес или источник могут не соответствовать шаблону напрямую. К ним относятся фреймы с различными схемами, такими как about: data: blob: и filesystem: См. также Внедрение в связанные фреймы . |
world | ExecutionWorld | Необязательный параметр. Область выполнения скрипта на JavaScript. По умолчанию используется ISOLATED . См. также «Работа в изолированных средах» . |
На определенном этапе жизненного цикла документа первыми внедряются скрипты содержимого, объявленные статически в манифесте, прежде чем внедряются скрипты содержимого, зарегистрированные любым другим способом. Они внедряются в том порядке, в котором указаны в манифесте.
Внедрение с помощью динамических объявлений
Динамические скрипты контента полезны в тех случаях, когда шаблоны соответствия для скриптов контента плохо известны или когда скрипты контента не всегда следует внедрять на известных хостах.
Введенные в Chrome 96, динамические объявления похожи на статические , но объект скрипта содержимого регистрируется в Chrome с помощью методов в пространстве имен chrome.scripting , а не в manifest.json . API скриптинга также позволяет разработчикам расширений:
- Зарегистрируйте скрипты контента.
- Получите список зарегистрированных скриптов контента.
- Обновите список зарегистрированных скриптов контента.
- Удалите зарегистрированные скрипты контента.
Подобно статическим объявлениям, динамические объявления могут включать файлы JavaScript, файлы CSS или и то, и другое.
service-worker.js
chrome.scripting
.registerContentScripts([{
id: "session-script",
js: ["content.js"],
persistAcrossSessions: false,
matches: ["*://example.com/*"],
runAt: "document_start",
}])
.then(() => console.log("registration complete"))
.catch((err) => console.warn("unexpected error", err))
service-worker.js
chrome.scripting
.updateContentScripts([{
id: "session-script",
excludeMatches: ["*://admin.example.com/*"],
}])
.then(() => console.log("registration updated"));
service-worker.js
chrome.scripting
.getRegisteredContentScripts()
.then(scripts => console.log("registered content scripts", scripts));
service-worker.js
chrome.scripting
.unregisterContentScripts({ ids: ["session-script"] })
.then(() => console.log("un-registration complete"));
Внедрение программным способом
Используйте программное внедрение для скриптов контента, которые должны запускаться в ответ на события или в определенных ситуациях.
Для программного внедрения скрипта содержимого вашему расширению необходимы права доступа к странице, на которую оно пытается внедрить скрипты. Права доступа можно предоставить либо запросив их в манифесте расширения, либо временно используя "activeTab" .
Ниже представлены различные версии расширения, основанного на ActiveTab.
manifest.json:
{
"name": "My extension",
...
"permissions": [
"activeTab",
"scripting"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_title": "Action Button"
}
}
Скрипты контента можно внедрять в виде файлов.
content-script.js
document.body.style.backgroundColor = "orange";
service-worker.js:
chrome.action.onClicked.addListener((tab) => {
chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ["content-script.js"]
});
});
Или же тело функции может быть внедрено и выполнено как скрипт содержимого.
service-worker.js:
function injectedFunction() {
document.body.style.backgroundColor = "orange";
}
chrome.action.onClicked.addListener((tab) => {
chrome.scripting.executeScript({
target : {tabId : tab.id},
func : injectedFunction,
});
});
Обратите внимание, что внедряемая функция является копией функции, на которую ссылается вызов chrome.scripting.executeScript() , а не самой исходной функцией. В результате тело функции должно быть самодостаточным; ссылки на переменные вне функции приведут к тому, что скрипт содержимого выдаст ReferenceError .
При внедрении в качестве функции вы также можете передавать аргументы этой функции.
service-worker.js
function injectedFunction(color) {
document.body.style.backgroundColor = color;
}
chrome.action.onClicked.addListener((tab) => {
chrome.scripting.executeScript({
target : {tabId : tab.id},
func : injectedFunction,
args : [ "orange" ],
});
});
Исключить спички и шарики
Для настройки соответствия указанным страницам включите следующие поля в декларативную регистрацию.
| Имя | Тип | Описание |
|---|---|---|
exclude_matches | массив строк | Необязательно. Исключает страницы, на которые в противном случае был бы внедрен этот скрипт содержимого. Подробнее о синтаксисе этих строк см. в разделе «Шаблоны соответствия» . |
include_globs | массив строк | Необязательно. Применяется после matches , чтобы включить только те URL-адреса, которые также соответствуют этому шаблону. Это предназначено для имитации ключевого слова @include Greasemonkey. |
exclude_globs | массив строк | Необязательный параметр. Применяется после matches для исключения URL-адресов, соответствующих этому шаблону. Предназначен для имитации ключевого слова Greasemonkey @exclude . |
Скрипт содержимого будет внедрен на страницу, если выполняются оба следующих условия:
- Его URL-адрес соответствует любому шаблону
matchesи любому шаблонуinclude_globs. - URL-адрес также не соответствует шаблону
exclude_matchesилиexclude_globs. Поскольку свойствоmatchesявляется обязательным,exclude_matches,include_globsиexclude_globsможно использовать только для ограничения круга страниц, которые будут затронуты.
Следующее расширение внедряет скрипт содержимого в https://www.nytimes.com/health , но не в https://www.nytimes.com/business .
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"exclude_matches": ["*://*/*business*"],
"js": ["contentScript.js"]
}
],
...
}
service-worker.js
chrome.scripting.registerContentScripts([{
id : "test",
matches : [ "https://*.nytimes.com/*" ],
excludeMatches : [ "*://*/*business*" ],
js : [ "contentScript.js" ],
}]);
Свойства шаблонов (glob) используют другой, более гибкий синтаксис, чем шаблоны соответствия . Допустимыми строками шаблонов являются URL-адреса, которые могут содержать звездочки и вопросительные знаки. Звездочка ( * ) соответствует любой строке любой длины, включая пустую строку, а вопросительный знак ( ? ) соответствует любому отдельному символу.
Например, шаблон https://???.example.com/foo/\* соответствует любому из следующих вариантов:
-
https://www.example.com/foo/bar -
https://the.example.com/foo/
Однако это не соответствует следующему:
-
https://my.example.com/foo/bar -
https://example.com/foo/ -
https://www.example.com/foo
Это расширение внедряет скрипт содержимого в страницы https://www.nytimes.com/arts/index.html и https://www.nytimes.com/jobs/index.htm* , но не в страницу https://www.nytimes.com/sports/index.html :
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"include_globs": ["*nytimes.com/???s/*"],
"js": ["contentScript.js"]
}
],
...
}
Это расширение внедряет скрипт содержимого на https://history.nytimes.com и https://.nytimes.com/history , но не на https://science.nytimes.com или https://www.nytimes.com/science .
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"exclude_globs": ["*science*"],
"js": ["contentScript.js"]
}
],
...
}
Для достижения необходимого масштаба можно включить один, все или некоторые из этих пунктов.
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"exclude_matches": ["*://*/*business*"],
"include_globs": ["*nytimes.com/???s/*"],
"exclude_globs": ["*science*"],
"js": ["contentScript.js"]
}
],
...
}
Время выполнения
Поле run_at определяет, когда файлы JavaScript внедряются на веб-страницу. Предпочтительное значение по умолчанию — "document_idle" . См. описание типа RunAt для получения информации о других возможных значениях.
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"run_at": "document_idle",
"js": ["contentScript.js"]
}
],
...
}
service-worker.js
chrome.scripting.registerContentScripts([{
id : "test",
matches : [ "https://*.nytimes.com/*" ],
runAt : "document_idle",
js : [ "contentScript.js" ],
}]);
| Имя | Тип | Описание |
|---|---|---|
document_idle | нить | Предпочтительно. По возможности используйте "document_idle" .Браузер выбирает момент для внедрения скриптов в промежутке между "document_end" и моментом сразу после срабатывания события window.onload . Точный момент внедрения зависит от сложности документа и времени его загрузки, и оптимизирован для скорости загрузки страницы.Скрипты, работающие в состоянии "document_idle" не обязаны отслеживать событие window.onload , поскольку гарантируется их выполнение после завершения формирования DOM-дерева. Если скрипту обязательно нужно выполниться после window.onload , расширение может проверить, сработало ли onload используя свойство document.readyState . |
document_start | нить | Скрипты внедряются после любых файлов из css , но до того, как будет создан какой-либо другой DOM-элемент или запущен какой-либо другой скрипт. |
document_end | нить | Скрипты внедряются сразу после завершения формирования DOM-дерева, но до загрузки таких вложенных ресурсов, как изображения и фреймы. |
Укажите рамки
Для декларативных скриптов содержимого, указанных в манифесте, поле "all_frames" позволяет расширению указать, следует ли внедрять файлы JavaScript и CSS во все фреймы, соответствующие указанным требованиям URL, или только в самый верхний фрейм во вкладке:
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"all_frames": true,
"js": ["contentScript.js"]
}
],
...
}
При программной регистрации скриптов содержимого с помощью chrome.scripting.registerContentScripts(...) параметр allFrames можно использовать для указания того, следует ли внедрять скрипт содержимого во все фреймы, соответствующие указанным требованиям URL, или только в самый верхний фрейм во вкладке. Это можно использовать только с tabId и нельзя использовать, если указаны frameIds или documentIds:
service-worker.js
chrome.scripting.registerContentScripts([{
id: "test",
matches : [ "https://*.nytimes.com/*" ],
allFrames : true,
js : [ "contentScript.js" ],
}]);
Внедрить в соответствующие фреймы
Расширения могут захотеть запускать скрипты во фреймах, которые связаны с соответствующим фреймом, но сами не соответствуют ему. Распространенный сценарий в этом случае — это фреймы с URL-адресами, созданными соответствующим фреймом, но URL-адреса которых сами не соответствуют указанным скриптом шаблонам.
Это происходит, когда расширение хочет внедрить фреймы с URL-адресами, имеющими схемы about: data: blob: и filesystem: :. В таких случаях URL-адрес не будет соответствовать шаблону скрипта содержимого (а в случае about: и data: даже не будет включать родительский URL или источник в URL-адрес, как в about:blank или data:text/html,<html>Hello, World!</html> ). Однако эти фреймы всё ещё могут быть связаны с создающим их фреймом.
Для внедрения в эти фреймы расширения могут указать свойство "match_origin_as_fallback" в спецификации скрипта содержимого в манифесте.
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.google.com/*"],
"match_origin_as_fallback": true,
"js": ["contentScript.js"]
}
],
...
}
Если этот параметр указан и установлен в true , Chrome будет определять соответствие фрейма источнику, инициатору фрейма, а не самому URL-адресу фрейма. Обратите внимание, что этот источник может отличаться от источника целевого фрейма (например, URL-адреса data: имеют пустой источник).
Инициатором фрейма является фрейм, который создал целевой фрейм или перешел по нему. Хотя обычно это непосредственный родительский фрейм или фрейм, открывающий фрейм, это может быть и не так (например, в случае, когда фрейм переходит по iframe внутри iframe).
Поскольку это сравнивает источник инициирующего кадра, инициирующий кадр может находиться по любому пути от этого источника. Чтобы это стало понятнее, Chrome требует, чтобы любые скрипты содержимого, указанные с параметром "match_origin_as_fallback" в значении true , также указывали путь * .
Если указаны одновременно "match_origin_as_fallback" и "match_about_blank" , то приоритет имеет параметр "match_origin_as_fallback" .
Взаимодействие со страницей встраивания
Хотя среды выполнения скриптов контента и страницы, на которых они размещены, изолированы друг от друга, они совместно используют DOM страницы. Если страница хочет взаимодействовать со скриптом контента или с расширением через скрипт контента, она должна делать это через общий DOM.
Пример можно реализовать с помощью window.postMessage() :
content-script.js
var port = chrome.runtime.connect();
window.addEventListener("message", (event) => {
// We only accept messages from ourselves
if (event.source !== window) {
return;
}
if (event.data.type && (event.data.type === "FROM_PAGE")) {
console.log("Content script received: " + event.data.text);
port.postMessage(event.data.text);
}
}, false);
пример.js
document.getElementById("theButton").addEventListener("click", () => {
window.postMessage(
{type : "FROM_PAGE", text : "Hello from the webpage!"}, "*");
}, false);
Страница, не являющаяся расширением, example.html, отправляет сообщения самой себе. Это сообщение перехватывается и анализируется скриптом содержимого, а затем отправляется в процесс расширения. Таким образом, страница устанавливает канал связи с процессом расширения. Обратный процесс возможен аналогичным образом.
Доступ к файлам расширений
Для доступа к файлу расширения из скрипта содержимого можно вызвать chrome.runtime.getURL() чтобы получить абсолютный URL-адрес ресурса вашего расширения, как показано в следующем примере ( content.js ):
content-script.js
let image = chrome.runtime.getURL("images/my_image.png")
Для использования шрифтов или изображений в CSS-файле можно использовать @@extension_id для формирования URL-адреса, как показано в следующем примере ( content.css ):
content.css
body {
background-image:url('chrome-extension://__MSG_@@extension_id__/background.png');
}
@font-face {
font-family: 'Stint Ultra Expanded';
font-style: normal;
font-weight: 400;
src: url('chrome-extension://__MSG_@@extension_id__/fonts/Stint Ultra Expanded.woff') format('woff');
}
Все ресурсы должны быть указаны в файле manifest.json как доступные через веб-интерфейс :
manifest.json
{
...
"web_accessible_resources": [
{
"resources": [ "images/*.png" ],
"matches": [ "https://example.com/*" ]
},
{
"resources": [ "fonts/*.woff" ],
"matches": [ "https://example.com/*" ]
}
],
...
}
Политика безопасности контента
Скрипты контента, работающие в изолированных средах, имеют следующую политику безопасности контента (CSP):
script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' chrome-extension://abcdefghijklmopqrstuvwxyz/; object-src 'self';
Подобно ограничениям, применяемым к другим контекстам расширений, это предотвращает использование функции eval() , а также загрузку внешних скриптов.
Для распакованных расширений CSP также включает localhost:
script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' http://localhost:* http://127.0.0.1:* chrome-extension://abcdefghijklmopqrstuvwxyz/; object-src 'self';
Когда скрипт контента внедряется в основной контент, применяются правила CSP этой страницы.
Оставайтесь в безопасности
Хотя изолированные миры обеспечивают дополнительный уровень защиты, использование скриптов контента может создавать уязвимости как в расширении, так и на веб-странице. Если скрипт контента получает контент с отдельного веб-сайта, например, вызывая функцию fetch() , следует тщательно фильтровать контент на предмет атак межсайтового скриптинга (XSS) перед его внедрением. Для предотвращения атак типа «человек посередине» используйте только протокол HTTPS.
Обязательно отфильтруйте страницы на наличие вредоносного контента. Например, следующие шаблоны опасны и запрещены в Manifest V3:
content-script.js
const data = document.getElementById("json-data"); // WARNING! Might be evaluating an evil script! const parsed = eval("(" + data + ")");
content-script.js
const elmt_id = ... // WARNING! elmt_id might be '); ... evil script ... //'! window.setTimeout("animate(" + elmt_id + ")", 200);
Вместо этого отдавайте предпочтение более безопасным API, которые не запускают скрипты:
content-script.js
const data = document.getElementById("json-data") // JSON.parse does not evaluate the attacker's scripts. const parsed = JSON.parse(data);
content-script.js
const elmt_id = ... // The closure form of setTimeout does not evaluate scripts. window.setTimeout(() => animate(elmt_id), 200);