Skrypty treści

Skrypty treści to pliki, które działają w kontekście stron internetowych. Za pomocą 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 korzystać z 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.

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

Praca w odizolowanych światach

Skrypty dotyczące zawartości działają w odizolowanym środowisku, co pozwala im wprowadzać zmiany w swoim środowisku JavaScriptu bez konfliktu ze stroną lub skryptami dotyczącymi zawartości innych rozszerzeń.

Rozszerzenie może działać na stronie internetowej z kodem podobnym do tego:

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 technik 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ą się wyświetlać kolejno.

Wstrzykiwanie skryptów

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

Wstrzyknięcie za pomocą statycznych deklaracji

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

Scenariusze zadeklarowane statycznie są rejestrowane w pliku manifestu pod kluczem "content_scripts". Mogą to być pliki JavaScript lub CSS albo oba te typy. Wszystkie skrypty treści uruchamiane automatycznie muszą zawierać 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 tekstowa Wymagany. Określa, na których stronach ma być wstrzykiwany 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 tekstowa Opcjonalnie. Lista plików CSS, które mają zostać wstrzyknięte na pasujące strony. Są one wstawiane w kolejności, w jakiej występują w tym tablicy, zanim zostanie utworzony lub wyświetlony DOM strony.
js tabelka tekstowa Opcjonalnie. Lista plików JavaScript, które mają zostać wstrzyknięte na pasujące strony. Pliki są wstrzykiwane w kolejności, w jakiej występują w tej tablicy. Każdy ciąg na tej liście musi zawierać ścieżkę względną do zasobu w katalogu głównym rozszerzenia. Początkowe ukośniki (/) są automatycznie obcinane.
run_at RunAt Opcjonalnie. Określa, kiedy skrypt powinien zostać wstrzyknięty na stronie. Domyślna wartość to document_idle.
match_about_blank wartość logiczna Opcjonalnie. Określa, czy skrypt ma zostać wstrzyknięty do elementu iframe about:blank, w którym element nadrzędny lub otwierający odpowiada jednemu z wzorów zadeklarowanych w elementzie matches. Wartość domyślna to fałsz.
match_origin_as_fallback wartość logiczna Opcjonalnie. Określa, czy skrypt ma być wstrzykiwany do ramek utworzonych przez dopasowane źródło, ale których adres URL lub źródło może 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 klatkach.
world ExecutionWorld Opcjonalnie. Środowisko JavaScriptu, w którym ma działać skrypt. Domyślna wartość to ISOLATED. Zobacz też: Praca w odizolowanych światach.

Wstrzykiwanie za pomocą deklaracji dynamicznych

Dynamiczne skrypty treści są przydatne, gdy wzorce dopasowania skryptów treści są mało znane lub gdy skrypty treści nie powinny być zawsze wstrzykiwane na znanych hostach.

Deklaracje dynamiczne zostały wprowadzone w Chrome 96. Są podobne do statycznych deklaracji, ale obiekt skryptu treści jest rejestrowany w Chrome za pomocą metod w przestrzeni nazw chrome.scripting, a nie w pliku manifest.json. Interfejs Scripting API umożliwia też deweloperom rozszerzeń:

Podobnie jak deklaracje statyczne, deklaracje dynamiczne mogą zawierać pliki JavaScript lub CSS albo oba te pliki.

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ą kodu

Za pomocą programowego wstrzykiwania możesz uruchamiać skrypty treści w odpowiedzi na zdarzenia lub w określonych sytuacjach.

Aby wstrzyknąć skrypt treści programowo, rozszerzenie musi mieć uprawnienia hosta na stronie, na której ma zostać wstrzyknięty skrypt. Uprawnienia dotyczące hosta można przyznać, prosząc o nie w ramach pliku manifestu rozszerzenia lub tymczasowo używając "activeTab".

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

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żesz też wstrzyknąć kod funkcji i wykonywać go 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 oryginalną funkcją. Dlatego ciało funkcji musi być samowystarczalne. Odwołania do zmiennych spoza funkcji spowodują, że skrypt treści wyrzuci błąd ReferenceError.

Podczas wstrzykiwania jako funkcji możesz też przekazywać jej 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 dopasowań nieprecyzyjnych

Aby dostosować dopasowywanie określonej strony, dodaj te pola w deklaratywnej rejestracji.

Nazwa Typ Opis
exclude_matches tablica tekstowa Opcjonalnie. Wyklucza strony, do których skrypt treści miałby zostać wstrzyknięty. Szczegółowe informacje o składni tych ciągów znajdziesz w artykule Wzorce dopasowania.
include_globs tablica tekstowa Opcjonalnie. Stosuje się go po matches, aby uwzględnić tylko te adresy URL, które pasują do tego wyrażenia regularnego. Ma to na celu emulowanie słowa kluczowego @include w Greasemonkey.
exclude_globs tablica tekstowa Opcjonalnie. Stosuje się go po znaku matches, aby wykluczyć adresy URL pasujące do tego zbioru. Słowo kluczowe Greasemonkey służące do naśladowania @exclude.

Skrypt treści zostanie wstrzyknięty na stronę, jeśli są spełnione 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 atrybuty exclude_matches, include_globsexclude_globs można używać tylko do ograniczania zakresu stron, na których mają one obowiązywać.

Podane niżej rozszerzenie wstrzykuje skrypt treś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

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

Właściwości globa mają inną, bardziej elastyczną składnię niż wzorce dopasowania. Dopuszczalne ciągi tekstowe to adresy URL, które mogą zawierać „symbole wieloznaczne” w postaci gwiazdek i znaków zapytania. Gwiazdka (*) odpowiada dowolnemu ciągu znaków o dowolnej długości, w tym pustemu ciągu, a znak zapytania (?) odpowiada dowolnemu pojedynczemu znakowi.

Na przykład wyrażenie https://???.example.com/foo/\* pasuje do dowolnego z tych elementów:

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

Nie odpowiada jednak:

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

To rozszerzenie wstrzykuje skrypt treś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ć jeden, wszystkie lub niektóre z tych elementów.

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 typie 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 ciąg znaków Preferowana. W miarę możliwości używaj tagu "document_idle".

Przeglądarka wybiera czas wstrzyknięcia skryptów między "document_end" a chwilą, gdy nastąpiło zdarzenie window.onload. Dokładny moment wstrzyknięcia zależy od złożoności dokumentu i czasu jego wczytywania. Jest on optymalizowany pod kątem szybkości wczytywania strony.

Skrypty treści działające w trybie "document_idle" nie muszą nasłuchiwać zdarzenia window.onload, ponieważ zawsze są wykonywane po zakończeniu wczytywania DOM. Jeśli skrypt musi działać dopiero po window.onload, rozszerzenie może sprawdzić, czy onload zostało już wywołane, korzystając z właściwości document.readyState.
document_start ciąg znaków Skrypty są wstrzykiwane po wszystkich plikach z folderu css, ale przed utworzeniem dowolnego innego DOM lub uruchomieniem dowolnego innego skryptu.
document_end ciąg znaków Skrypty są wstrzykiwane natychmiast po utworzeniu modelu DOM, ale przed załadowaniem zasobów podrzędnych, takich jak obrazy i ramki.

Określanie ramek

W przypadku skryptów deklaratywnych 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ń adresu URL, czy tylko do najwyższej ramki w karcie:

manifest.json

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

Podczas rejestrowania skryptów treści za pomocą kodu za pomocą chrome.scripting.registerContentScripts(...) możesz użyć parametru allFrames, aby określić, czy skrypt treści ma być wstrzykiwany do wszystkich ramek pasujących do określonych wymagań adresu URL, czy tylko do najwyższej ramki w karcie. Można go używać tylko z identyfikatorem tabId. Nie można go używać, jeśli określone są identyfikatory frameId lub documentId:

service-worker.js

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

Rozszerzenia mogą uruchamiać skrypty w ramkach, które są powiązane z ramką pasującą do nich, ale same do niej nie pasują. Typowym scenariuszem jest sytuacja, gdy ramki z adresami URL utworzonymi przez pasującą ramkę nie pasują do wzorców określonych w skrypcie.

Dzieje się tak, gdy rozszerzenie chce wstrzyknąć ramki z adresami URL o schematach 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 nadrzędnego ani źródła, 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 wstrzyknąć treści 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 ustawienie jest określone i wyrażone jako true, Chrome sprawdza pochodzenie inicjatora ramki, aby określić, czy dana ramka jest zgodna, a nie adres URL samej ramki. Pamiętaj, że może być ona inna niż pochodzenie ramki docelowej (np. Adresy URL data: mają null jako źródło.

Inicjator ramki to rama, która utworzyła lub nawigowała do ramki docelowej. Zwykle jest to element nadrzędny lub element otwierający, ale nie zawsze (np. w przypadku ramki przechodzącej do elementu iframe w ramce iframe).

Ponieważ porównuje to pochodzenie ramki inicjującej, może ona znajdować się na dowolnej ścieżce od tego punktu. Aby to wyjaśnić, Chrome wymaga, aby skrypty treści określone za pomocą "match_origin_as_fallback" ustawione na true, również określały ścieżkę *.

Jeśli podane są zarówno parametry "match_origin_as_fallback", jak i "match_about_blank", parametr "match_origin_as_fallback" ma pierwszeństwo.

Komunikacja ze stroną, na której znajduje się element docelowy

Chociaż środowiska wykonywania skryptów treści i stron, na których są one hostowane, są od siebie odizolowane, mają wspólny dostęp do modelu DOM strony. Jeśli strona chce komunikować się ze skryptem treści lub z rozszerzeniem za pomocą skryptu treści, musi to robić za pomocą udostępnionego DOM.

Przykładowe użycie 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 wiadomości do siebie. Skrypt treści przechwytuje i sprawdza tę wiadomość, a następnie przekazuje ją do procesu rozszerzenia. W ten sposób strona umożliwia komunikację w ramach procesu rozszerzania. Odwrotne przekształcenie jest możliwe za pomocą podobnych metod.

Dostęp do plików rozszerzeń

Aby uzyskać dostęp do pliku rozszerzenia z poziomu skryptu treści, możesz wywołać funkcję chrome.runtime.getURL(), aby uzyskać 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ć funkcji @@extension_id, aby utworzyć adres URL w sposób pokazany 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 zasoby muszą być zadeklarowane jako zasoby dostępne w sieci w pliku manifest.json:

manifest.json

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

Zadbaj o bezpieczeństwo

Chociaż izolowane światy zapewniają pewien poziom ochrony, skrypty dotyczące zawartości mogą tworzyć luki w zabezpieczeniach rozszerzenia i strony internetowej. Jeśli skrypt treści otrzymuje treści z innej witryny, np. przez wywołanie funkcji fetch(), przed wstrzyknięciem treści należy je przefiltrować, aby zapobiec atakom XSS. Komunikacja powinna odbywać się tylko przez 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 wybieraj bezpieczniejsze interfejsy 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);