Skrypty dotyczące zawartości

Skrypty treści to pliki, które działają w kontekście stron internetowych. Korzystając ze standardowego modelu DOM, mogą odczytywać szczegóły stron internetowych odwiedzanych przez przeglądarkę, wprowadzać w nich zmiany i przekazywać informacje do rozszerzenia nadrzędnego.

Możliwości skryptów treści

Skrypty treści mogą bezpośrednio uzyskiwać dostęp do tych interfejsów API rozszerzeń:

Skrypty treści nie mogą bezpośrednio uzyskiwać dostępu do innych interfejsów API. Mogą jednak uzyskiwać do nich dostęp pośrednio, wymieniając wiadomości z innymi częściami rozszerzenia.

Ze skryptu treści możesz też uzyskać dostęp do innych plików w rozszerzeniu za pomocą interfejsów API, takich jak fetch(). Aby to zrobić, musisz zadeklarować je jako zasoby dostępne w internecie. Pamiętaj, że udostępnia to zasoby wszystkim skryptom własnym i firm zewnętrznych działającym w tej samej witrynie.

Praca w odizolowanych światach

Skrypty dotyczące zawartości działają w izolowanym środowisku, dzięki czemu mogą wprowadzać zmiany w środowisku JavaScript bez powodowania konfliktów ze stroną lub skryptami dotyczącymi zawartości innych rozszerzeń.

Rozszerzenie może działać na stronie internetowej z kodem podobnym do tego w przykładzie poniżej.

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>

Rozszerzenie może wstrzyknąć ten skrypt treści, korzystając z jednej z metod opisanych w sekcji Wstrzykiwanie skryptów.

content-script.js

var greeting = "hola, ";
var button = document.getElementById("mybutton");
button.person_name = "Roberto";
button.addEventListener(
    "click", () => alert(greeting + button.person_name + "."), false);

Po wprowadzeniu tej zmiany oba alerty będą wyświetlane po kolei po kliknięciu przycisku.

Wstrzykiwanie skryptów

Skrypty treści mogą być deklarowane statycznie, deklarowane dynamicznie lub wstrzykiwane programowo.

Wstrzykiwanie za pomocą deklaracji statycznych

W przypadku skryptów, które powinny być automatycznie uruchamiane na znanym zestawie stron, używaj deklaracji skryptów treści statycznych w pliku manifest.json.

Skrypty zadeklarowane statycznie są rejestrowane w pliku manifestu pod kluczem "content_scripts". Mogą one zawierać pliki JavaScript, pliki CSS lub oba te rodzaje plików. Wszystkie skrypty treści uruchamiane automatycznie muszą określać wzorce dopasowania.

manifest.json

{
 "name": "My extension",
 ...
 "content_scripts": [
   {
     "matches": ["https://*.nytimes.com/*"],
     "css": ["my-styles.css"],
     "js": ["content-script.js"]
   }
 ],
 ...
}

Nazwa Typ Opis
matches tablica ciągów znaków Parametr wymagany. Określa, na których stronach ma zostać wstrzyknięty ten skrypt treści. Szczegółowe informacje o składni tych ciągów znaków znajdziesz w artykule Wzorce dopasowania, a informacje o wykluczaniu adresów URL – w artykule Wzorce dopasowania i wieloznaki.
css tablica ciągów znaków Opcjonalnie. Lista plików CSS, które mają być wstrzykiwane na pasujące strony. Są one wstrzykiwane w kolejności, w jakiej występują w tej tablicy, zanim zostanie utworzony lub wyświetlony jakikolwiek DOM strony.
js tablica ciągów znaków Opcjonalny. Lista plików JavaScript, które mają być wstrzykiwane na pasujące strony. Pliki są wstawiane w kolejności, w jakiej występują w tej tablicy. Każdy ciąg tekstowy na tej liście musi zawierać ścieżkę względną do zasobu w katalogu głównym rozszerzenia. Początkowe ukośniki (`/`) są automatycznie usuwane.
run_at RunAt Opcjonalnie. Określa, kiedy skrypt ma zostać umieszczony na stronie. Domyślna wartość to document_idle.
match_about_blank wartość logiczna Opcjonalnie. Określa, czy skrypt ma być wstrzykiwany do ramki about:blank, w której ramka nadrzędna lub otwierająca pasuje do jednego z wzorców zadeklarowanych w matches. Wartość domyślna to fałsz.
match_origin_as_fallback wartość logiczna Opcjonalnie. Określa, czy skrypt ma być wstrzykiwany w ramki utworzone przez pasującą domenę, ale których adres URL lub domena mogą nie pasować bezpośrednio do wzorca. Obejmują one ramki z różnymi schematami, takimi jak about:, data:, blob:filesystem:. Zobacz też Wstrzykiwanie w powiązanych ramkach.
world ExecutionWorld Opcjonalnie. Środowisko JavaScript, w którym ma być wykonywany skrypt. Domyślna wartość to ISOLATED. Zobacz też Praca w izolowanych światach.

Na danym etapie cyklu życia dokumentu najpierw wstrzykiwane są skrypty treści zadeklarowane statycznie w pliku manifestu, a potem skrypty treści zarejestrowane w inny sposób. Są one wstrzykiwane w kolejności, w jakiej zostały określone w pliku manifestu.

Wstrzykiwanie za pomocą deklaracji dynamicznych

Skrypty treści dynamicznych są przydatne, gdy wzorce dopasowania skryptów treści nie są dobrze znane lub gdy skrypty treści nie powinny być zawsze wstrzykiwane na znanych hostach.

Wprowadzone w Chrome 96 deklaracje dynamiczne są podobne do deklaracji statycznych, ale obiekt skryptu treści jest rejestrowany w Chrome za pomocą metod w przestrzeni nazwbrowser.scripting, a nie w manifest.json. Interfejs Scripting API umożliwia też programistom rozszerzeń:

Podobnie jak deklaracje statyczne, deklaracje dynamiczne mogą zawierać pliki JavaScript, pliki CSS lub oba te rodzaje plików.

service-worker.js

browser.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

browser.scripting
  .updateContentScripts([{
    id: "session-script",
    excludeMatches: ["*://admin.example.com/*"],
  }])
  .then(() => console.log("registration updated"));

service-worker.js

browser.scripting
  .getRegisteredContentScripts()
  .then(scripts => console.log("registered content scripts", scripts));

service-worker.js

browser.scripting
  .unregisterContentScripts({ ids: ["session-script"] })
  .then(() => console.log("un-registration complete"));

Wstawianie za pomocą kodu

Wstrzykiwanie programowe stosuj w przypadku skryptów treści, które muszą być uruchamiane w odpowiedzi na zdarzenia lub w określonych sytuacjach.

Aby wstrzyknąć skrypt dotyczący zawartości programowo, rozszerzenie musi mieć uprawnienia hosta do strony, na której próbuje wstrzyknąć skrypty. Uprawnienia do hosta można przyznać, prosząc o nie w ramach manifestu rozszerzenia lub tymczasowo za pomocą "activeTab".

Poniżej znajdziesz różne wersje rozszerzenia opartego na activeTab.

manifest.json:

{
  "name": "My extension",
  ...
  "permissions": [
    "activeTab",
    "scripting"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_title": "Action Button"
  }
}

Skrypty treści można wstrzykiwać jako pliki.

content-script.js


document.body.style.backgroundColor = "orange";

service-worker.js:

browser.action.onClicked.addListener((tab) => {
  browser.scripting.executeScript({
    target: { tabId: tab.id },
    files: ["content-script.js"]
  });
});

Można też wstrzyknąć treść funkcji i wykonać ją jako skrypt treści.

service-worker.js:

function injectedFunction() {
  document.body.style.backgroundColor = "orange";
}

browser.action.onClicked.addListener((tab) => {
  browser.scripting.executeScript({
    target : {tabId : tab.id},
    func : injectedFunction,
  });
});

Pamiętaj, że wstrzyknięta funkcja jest kopią funkcji, do której odwołuje się wywołanie browser.scripting.executeScript(), a nie samą funkcją. W związku z tym treść funkcji musi być samodzielna. Odwołania do zmiennych poza funkcją spowodują, że skrypt treści zgłosi błąd ReferenceError.

Podczas wstrzykiwania jako funkcji możesz też przekazywać do niej argumenty.

service-worker.js

function injectedFunction(color) {
  document.body.style.backgroundColor = color;
}

browser.action.onClicked.addListener((tab) => {
  browser.scripting.executeScript({
    target : {tabId : tab.id},
    func : injectedFunction,
    args : [ "orange" ],
  });
});

Wykluczanie dopasowań i wzorców

Aby dostosować dopasowywanie określonych stron, w deklaratywnej rejestracji uwzględnij te pola.

Nazwa Typ Opis
exclude_matches tablica ciągów znaków Opcjonalnie. Wyklucza strony, do których w inny sposób zostałby wstrzyknięty ten skrypt treści. Szczegółowe informacje o składni tych ciągów znaków znajdziesz w artykule Wzorce dopasowania.
include_globs tablica ciągów znaków Opcjonalnie. Stosowany po znaku matches, aby uwzględniać tylko te adresy URL, które pasują też do tego wzorca. Ma to na celu emulację słowa kluczowego @include Greasemonkey.
exclude_globs tablica ciągów znaków Opcjonalnie. Stosowane po znaku matches, aby wykluczyć adresy URL pasujące do tego wzorca. Ma naśladować słowo kluczowe @exclude Greasemonkey.

Skrypt treści zostanie wstrzyknięty na stronę, jeśli spełnione są oba te warunki:

  • Jego adres URL pasuje do dowolnego wzorca matches i dowolnego wzorca include_globs.
  • Adres URL nie pasuje też do wzorca exclude_matches ani exclude_globs. Właściwość matches jest wymagana, więc właściwości exclude_matches, include_globsexclude_globs można stosować tylko do ograniczania liczby stron, których dotyczy problem.

Poniższe rozszerzenie wstrzykuje skrypt dotyczący zawartości do pliku https://www.nytimes.com/health, ale nie do pliku 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

browser.scripting.registerContentScripts([{
  id : "test",
  matches : [ "https://*.nytimes.com/*" ],
  excludeMatches : [ "*://*/*business*" ],
  js : [ "contentScript.js" ],
}]);

Właściwości glob mają inną, bardziej elastyczną składnię niż wzorce dopasowania. Dopuszczalne ciągi glob to adresy URL, które mogą zawierać gwiazdki i znaki zapytania pełniące rolę symboli wieloznacznych. Gwiazdka (*) odpowiada dowolnemu ciągowi znaków o dowolnej długości, w tym pustemu ciągowi znaków, a znak zapytania (?) odpowiada dowolnemu pojedynczemu znakowi.

Na przykład wzorzec https://???.example.com/foo/\* pasuje do dowolnego z tych ciągów znaków:

  • https://www.example.com/foo/bar
  • https://the.example.com/foo/

Nie pasuje jednak do tych wyrażeń:

  • https://my.example.com/foo/bar
  • https://example.com/foo/
  • https://www.example.com/foo

To rozszerzenie wstrzykuje skrypt dotyczący zawartości do https://www.nytimes.com/arts/index.html i https://www.nytimes.com/jobs/index.htm*, ale nie do 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"]
    }
  ],
  ...
}

To rozszerzenie wstrzykuje skrypt treści do https://history.nytimes.comhttps://.nytimes.com/history, ale nie do https://science.nytimes.com ani https://www.nytimes.com/science:

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "exclude_globs": ["*science*"],
      "js": ["contentScript.js"]
    }
  ],
  ...
}

Aby uzyskać odpowiedni zakres, możesz uwzględnić wszystkie te elementy, niektóre z nich lub tylko jeden.

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"]
    }
  ],
  ...
}

Czas trwania

Pole run_at określa, kiedy pliki JavaScript są wstrzykiwane na stronę internetową. Preferowaną i domyślną wartością jest "document_idle". Inne możliwe wartości znajdziesz w sekcji typu RunAt.

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "run_at": "document_idle",
      "js": ["contentScript.js"]
    }
  ],
  ...
}

service-worker.js

browser.scripting.registerContentScripts([{
  id : "test",
  matches : [ "https://*.nytimes.com/*" ],
  runAt : "document_idle",
  js : [ "contentScript.js" ],
}]);
Nazwa Typ Opis
document_idle tekst Preferowana Używaj "document_idle", gdy tylko jest to możliwe.

Przeglądarka wybiera czas na wstrzyknięcie skryptów między "document_end" a bezpośrednio po uruchomieniu zdarzenia window.onload. Dokładny moment wstrzyknięcia zależy od złożoności dokumentu i czasu wczytywania oraz jest zoptymalizowany pod kątem szybkości wczytywania strony.

Skrypty treści działające w momencie "document_idle" nie muszą nasłuchiwać zdarzenia window.onload – mają gwarancję, że zostaną uruchomione po ukończeniu DOM. Jeśli skrypt musi zostać uruchomiony po zdarzeniu window.onload, rozszerzenie może sprawdzić, czy zdarzenie onload zostało już wywołane, korzystając z właściwości document.readyState.
document_start tekst Skrypty są wstrzykiwane po wszystkich plikach z css, ale przed utworzeniem jakiegokolwiek innego DOM lub uruchomieniem jakiegokolwiek innego skryptu.
document_end tekst Skrypty są wstrzykiwane natychmiast po zakończeniu wczytywania modelu DOM, ale przed załadowaniem zasobów podrzędnych, takich jak obrazy i ramki.

Określanie ramek

W przypadku deklaratywnych skryptów treści określonych w pliku manifestu pole "all_frames" umożliwia rozszerzeniu określenie, czy pliki JavaScript i CSS mają być wstrzykiwane do wszystkich ramek pasujących do określonych wymagań dotyczących adresu URL, czy tylko do ramki najwyższego poziomu na karcie:

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "all_frames": true,
      "js": ["contentScript.js"]
    }
  ],
  ...
}

Podczas programowego rejestrowania skryptów treści za pomocą interfejsu browser.scripting.registerContentScripts(...) można użyć parametru allFrames, aby określić, czy skrypt treści ma być wstrzykiwany do wszystkich ramek pasujących do określonych wymagań dotyczących adresu URL, czy tylko do ramki najwyższego poziomu na karcie. Można jej używać tylko z parametrem tabId. Nie można jej używać, jeśli określono parametry frameIds lub documentIds:

service-worker.js

browser.scripting.registerContentScripts([{
  id: "test",
  matches : [ "https://*.nytimes.com/*" ],
  allFrames : true,
  js : [ "contentScript.js" ],
}]);

Rozszerzenia mogą chcieć uruchamiać skrypty w ramkach powiązanych z pasującą ramką, ale niepasujących do niej. Często zdarza się to w przypadku ramek z adresami URL, które zostały utworzone przez pasującą ramkę, ale których adresy URL nie pasują do wzorców określonych w skrypcie.

Dzieje się tak, gdy rozszerzenie chce wstrzyknąć kod do ramek z adresami URL, które mają schematy about:, data:, blob:filesystem:. W takich przypadkach adres URL nie będzie pasować do wzorca skryptu treści (a w przypadku about:data: nie będzie nawet zawierać adresu URL ani pochodzenia elementu nadrzędnego, jak w przypadku about:blank lub data:text/html,<html>Hello, World!</html>). Te ramki mogą być jednak nadal powiązane z ramką, która je utworzyła.

Aby wstawiać treści do tych ramek, rozszerzenia mogą określać właściwość "match_origin_as_fallback" w specyfikacji skryptu treści w pliku manifestu.

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.google.com/*"],
      "match_origin_as_fallback": true,
      "js": ["contentScript.js"]
    }
  ],
  ...
}

Jeśli ta opcja jest określona i ma wartość true, Chrome będzie sprawdzać pochodzenie inicjatora ramki, aby określić, czy ramka pasuje, zamiast sprawdzać adres URL samej ramki. Pamiętaj, że może się on różnić od źródła ramki docelowej (np. adresy URL data: mają źródło o wartości null).

Inicjatorem ramki jest ramka, która utworzyła ramkę docelową lub do niej przeszła. Zwykle jest to bezpośredni element nadrzędny lub otwierający, ale nie zawsze (np. w przypadku ramki nawigującej w ramce iframe w ramce iframe).

Ponieważ porównywane jest źródło klatki inicjującej, może ona znajdować się w dowolnej ścieżce od tego źródła. Aby to było jasne, Chrome wymaga, aby wszystkie skrypty treści określone za pomocą "match_origin_as_fallback" ustawionego na true określały też ścieżkę *.

Jeśli podano zarówno "match_origin_as_fallback", jak i "match_about_blank", pierwszeństwo ma "match_origin_as_fallback".

Komunikacja ze stroną, na której jest osadzony

Środowiska wykonawcze skryptów treści i stron, które je hostują, są od siebie odizolowane, ale mają wspólny dostęp do DOM strony. Jeśli strona chce komunikować się ze skryptem treści lub z rozszerzeniem za jego pomocą, musi to robić za pomocą wspólnego DOM.

Przykład można uzyskać za pomocą funkcji window.postMessage():

content-script.js

var port = browser.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);

example.js

document.getElementById("theButton").addEventListener("click", () => {
  window.postMessage(
      {type : "FROM_PAGE", text : "Hello from the webpage!"}, "*");
}, false);

Strona bez rozszerzenia, example.html, wysyła do siebie wiadomości. Ta wiadomość jest przechwytywana i sprawdzana przez skrypt treści, a następnie wysyłana do procesu rozszerzenia. W ten sposób strona nawiązuje komunikację z procesem rozszerzenia. Można to zrobić w podobny sposób.

Dostęp do plików rozszerzenia

Aby uzyskać dostęp do pliku rozszerzenia ze skryptu treści, możesz wywołać funkcję browser.runtime.getURL(), aby pobrać bezwzględny adres URL zasobu rozszerzenia, jak pokazano w tym przykładzie (content.js):

content-script.js

let image = browser.runtime.getURL("images/my_image.png")

Aby używać czcionek lub obrazów w pliku CSS, możesz użyć @@extension_id do utworzenia adresu URL, jak pokazano w tym przykładzie (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');
}

Wszystkie komponenty muszą być zadeklarowane jako zasoby dostępne w internecie w pliku manifest.json:

manifest.json

{
 ...
 "web_accessible_resources": [
   {
     "resources": [ "images/*.png" ],
     "matches": [ "https://example.com/*" ]
   },
   {
     "resources": [ "fonts/*.woff" ],
     "matches": [ "https://example.com/*" ]
   }
 ],
 ...
}

(standard) Content Security Policy

Skrypty treści działające w izolowanych światach mają następującą politykę bezpieczeństwa treści (CSP):

script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' chrome-extension://abcdefghijklmopqrstuvwxyz/; object-src 'self';

Podobnie jak w przypadku ograniczeń stosowanych w innych kontekstach rozszerzeń, uniemożliwia to używanie eval() oraz wczytywanie skryptów zewnętrznych.

W przypadku rozpakowanych rozszerzeń zasady CSP obejmują też adres localhost:

script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' http://localhost:* http://127.0.0.1:* chrome-extension://abcdefghijklmopqrstuvwxyz/; object-src 'self';

Gdy skrypt treści jest wstrzykiwany do głównego świata, obowiązuje zasada CSP strony.

Zadbaj o bezpieczeństwo

Odizolowane światy zapewniają pewną ochronę, ale używanie skryptów treści może powodować luki w zabezpieczeniach rozszerzenia i strony internetowej. Jeśli skrypt treści otrzymuje treści z innej witryny, np. przez wywołanie fetch(), przed wstrzyknięciem treści należy je przefiltrować pod kątem ataków typu cross-site scripting. Komunikuj się tylko za pomocą protokołu HTTPS, aby uniknąć ataków typu "man-in-the-middle".

Pamiętaj, aby filtrować złośliwe strony internetowe. Na przykład te wzorce są niebezpieczne i niedozwolone w (platformie) Manifest V3:

Nie

content-script.js

const data = document.getElementById("json-data");
// WARNING! Might be evaluating an evil script!
const parsed = eval("(" + data + ")");
Nie

content-script.js

const elmt_id = ...
// WARNING! elmt_id might be '); ... evil script ... //'!
window.setTimeout("animate(" + elmt_id + ")", 200);

Zamiast tego używaj bezpieczniejszych interfejsów API, które nie uruchamiają skryptów:

Tak

content-script.js

const data = document.getElementById("json-data")
// JSON.parse does not evaluate the attacker's scripts.
const parsed = JSON.parse(data);
Tak

content-script.js

const elmt_id = ...
// The closure form of setTimeout does not evaluate scripts.
window.setTimeout(() => animate(elmt_id), 200);