סקריפטים של תוכן

סקריפטים של תוכן הם קבצים שפועלים בהקשר של דפי אינטרנט. באמצעות Document Object Model (DOM) הסטנדרטי, הם יכולים לקרוא פרטים של דפי האינטרנט שהדפדפן מבקר בהם, לבצע בהם שינויים ולהעביר מידע לתוסף ההורה שלהם.

הסבר על היכולות של סקריפטים של תוכן

סקריפטים של תוכן יכולים לגשת ישירות לממשקי ה-API הבאים של התוספים:

סקריפטים של תוכן לא יכולים לגשת ישירות לממשקי 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 מופיע במאמר תבניות התאמה וביטויים כלליים (glob).
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. בנוסף, Scripting 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(), ולא הפונקציה המקורית עצמה. כתוצאה מכך, גוף הפונקציה חייב להיות עצמאי. הפניות למשתנים מחוץ לפונקציה יגרמו ל-script התוכן להפעיל שגיאת 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" ],
  });
});

החרגת התאמות ו-globs

כדי להתאים אישית התאמה של דפים ספציפיים, צריך לכלול את השדות הבאים ברישום מילולי.

שם סוג תיאור
exclude_matches מערך מחרוזות אופציונלי. החרגה של דפים שבהם סקריפט התוכן הזה היה מוחדר אחרת. פרטים על התחביר של המחרוזות האלה מופיעים בקטע תבניות התאמה.
include_globs מערך מחרוזות אופציונלי. הקוד הזה מופעל אחרי matches כדי לכלול רק את כתובות ה-URL שתואמות גם לביטוי ה-glob הזה. המטרה היא לחקות את מילת המפתח @include של Greasemonkey.
exclude_globs מערך של מחרוזות אופציונלי. הקוד הזה מופעל אחרי matches כדי להחריג כתובות URL שתואמות לביטוי ה-glob הזה. מיועדת לחקות את מילת המפתח @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" ],
}]);

למאפייני Glob יש תחביר שונה וגמיש יותר מזה של דפוסי התאמה. מחרוזות glob מקובלות הן כתובות URL שעשויות להכיל כוכבית ותווים כלליים לחיפוש (wildcard) וסימני שאלה. הכוכבית (*) תואמת לכל מחרוזת בכל אורך, כולל מחרוזת ריקה, ואילו סימן השאלה (?) תואם לכל תו יחיד.

לדוגמה, ביטוי ה-glob 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: כוללות מקור null).

הגורם שמתחיל את הפריימים הוא הפריימים שיצר את פריימים היעד או ניווט אליהם. בדרך כלל מדובר בהורה הישיר או ברכיב שפתח את הדף, אבל יכול להיות שזה לא המצב (למשל, במקרה של מסגרת שמנווטת ב-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);

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

אבטחת החשבון

עולמות מבודדים מספקים שכבת הגנה, אבל שימוש בסקריפטים של תוכן עלול ליצור נקודות חולשה בתוסף ובדף האינטרנט. אם סקריפט התוכן מקבל תוכן מאתר נפרד, למשל באמצעות קריאה ל-fetch(), חשוב לסנן את התוכן מפני התקפות של סקריפטים באתרים שונים לפני ההזרקה שלו. צריך לתקשר רק דרך HTTPS כדי למנוע התקפות "man-in-the-middle".

חשוב לסנן דפי אינטרנט זדוניים. לדוגמה, התבניות הבאות מסוכנות ואסור להשתמש בהן ב-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);