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 mają bezpośredni 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 uzyskać do nich dostęp pośrednio, wymieniając wiadomości z innymi częściami rozszerzenia.

Za pomocą skryptu treści możesz też uzyskiwać dostęp do innych plików w rozszerzeniu, korzystając z 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 izolowanych światach

Skrypty dotyczące zawartości działają w izolowanym środowisku, co pozwala im 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 poniższym przykładzie.

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 po kliknięciu przycisku oba alerty będą wyświetlane kolejno.

Wstrzykiwanie skryptów

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

Wstawianie za pomocą deklaracji statycznych

W przypadku skryptów, które powinny być automatycznie uruchamiane na znanym zestawie stron, używaj statycznych deklaracji skryptów dotyczących treści 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 Wymagane. 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 wzorce glob.
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 tekstowych Opcjonalnie. 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ż Wstawianie 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.

Wstawianie 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 nazw chrome.scripting, a nie w manifest.json. Interfejs Scripting API umożliwia też deweloperom rozszerzeń:

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

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"));

Wstawianie za pomocą rozwiązań programowych

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 dotyczące hosta można przyznać, prosząc o nie w ramach pliku 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:

chrome.action.onClicked.addListener((tab) => {
  chrome.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";
}

chrome.action.onClicked.addListener((tab) => {
  chrome.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 chrome.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;
}

chrome.action.onClicked.addListener((tab) => {
  chrome.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 innych okolicznościach 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. Zastosowano po 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 tekstowych 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 exclude_matches, include_globsexclude_globs można używać tylko do ograniczania liczby stron, na których będą widoczne zmiany.

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

Właściwości wzorca 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.htmlhttps://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 z nich.

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

chrome.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 zakończeniu wczytywania 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 modelu DOM lub uruchomieniem 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 chrome.scripting.registerContentScripts(...) możesz użyć parametru allFrames, aby określić, czy skrypt treści ma być wstrzykiwany do wszystkich ramek spełniających wymagania określonego 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

chrome.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 same nie pasują. 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 można jednak nadal powiązać z ramką tworzącą.

Aby wstrzykiwać skrypty do tych ramek, rozszerzenia mogą określić 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 to pole jest określone i ma wartość true, Chrome sprawdza pochodzenie inicjatora ramki, aby określić, czy ramka pasuje, zamiast sprawdzać adres URL samej ramki. Pamiętaj, że może się to 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, która nawiguje w ramce iframe w ramce iframe).

Porównuje to pochodzenie klatki inicjującej, więc klatka inicjująca może znajdować się w dowolnej ścieżce od tego pochodzenia. Aby to było jasne, Chrome wymaga, aby wszystkie skrypty treści określone za pomocą "match_origin_as_fallback" ustawionego na true miały też ścieżkę *.

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

Komunikacja ze stroną osadzającą

Ś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 = 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);

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 odwrotną stronę za pomocą podobnych środków.

Dostęp do plików rozszerzenia

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

content-script.js

let image = chrome.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

Izolowane światy zapewniają pewną ochronę, ale używanie skryptów dotyczących zawartoś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ć, aby zapobiec atakom 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 pliku manifestu w wersji 3:

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:

Co należy robić

content-script.js

const data = document.getElementById("json-data")
// JSON.parse does not evaluate the attacker's scripts.
const parsed = JSON.parse(data);
Co należy robić

content-script.js

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