Скрипты контента — это файлы, которые запускаются в контексте веб-страниц. Используя стандартную объектную модель документов (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>
Это расширение может внедрить следующий скрипт содержимого, используя один из методов, описанных в разделе Внедрение скриптов .
контент-скрипт.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"
}
}
Скрипты контента можно внедрять в виде файлов.
контент-скрипт.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-адресов, соответствующих этому глобу. Предназначено для эмуляции ключевого слова @exclude Greasemonkey. |
Скрипт контента будет внедрен на страницу, если выполняются оба следующих условия:
- Его 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" ],
}]);
Свойства глобуса используют другой, более гибкий синтаксис, чем шаблоны сопоставления . Допустимые строки глобуса — это 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 самого фрейма. Обратите внимание, что он может отличаться от источника целевого фрейма (например, data:
URL имеют нулевой источник).
Инициатором фрейма является фрейм, создавший целевой фрейм или управлявший им. Хотя обычно это непосредственный родительский элемент или открыватель, это может быть и не так (например, в случае, когда фрейм управляет iframe внутри iframe).
Поскольку при этом сравнивается источник кадра-инициатора, кадр-инициатор может находиться по любому пути от этого источника. Чтобы сделать это очевидным, Chrome требует, чтобы все скрипты контента, указанные с параметром "match_origin_as_fallback"
в значении true
, также указывали путь *
.
Если указаны оба "match_origin_as_fallback"
и "match_about_blank"
, приоритет имеет "match_origin_as_fallback"
.
Связь со страницей встраивания
Хотя среды выполнения скриптов контента и страницы, на которых они размещены, изолированы друг от друга, они имеют общий доступ к DOM страницы. Если страница хочет взаимодействовать со скриптом контента или с расширением через скрипт контента, она должна делать это через общий DOM.
Пример можно реализовать с помощью window.postMessage()
:
контент-скрипт.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
):
контент-скрипт.js
let image = chrome.runtime.getURL("images/my_image.png")
Чтобы использовать шрифты или изображения в CSS-файле, вы можете использовать @@extension_id
для создания URL-адреса, как показано в следующем примере ( content.css
):
контент.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()
, перед внедрением обязательно отфильтруйте контент от атак межсайтового скриптинга . Используйте только HTTPS-соединение, чтобы избежать атак типа «человек посередине» .
Обязательно отфильтруйте вредоносные веб-страницы. Например, следующие шаблоны являются опасными и запрещены в Manifest V3:
контент-скрипт.js
const data = document.getElementById("json-data"); // WARNING! Might be evaluating an evil script! const parsed = eval("(" + data + ")");
контент-скрипт.js
const elmt_id = ... // WARNING! elmt_id might be '); ... evil script ... //'! window.setTimeout("animate(" + elmt_id + ")", 200);
Вместо этого отдавайте предпочтение более безопасным API, которые не запускают скрипты:
контент-скрипт.js
const data = document.getElementById("json-data") // JSON.parse does not evaluate the attacker's scripts. const parsed = JSON.parse(data);
контент-скрипт.js
const elmt_id = ... // The closure form of setTimeout does not evaluate scripts. window.setTimeout(() => animate(elmt_id), 200);