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

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

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

סקריפטים של תוכן יכולים לגשת ישירות לממשקי ה-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 מערך של מחרוזות חובה. מציין באילו דפים יוזרק סקריפט התוכן הזה. במאמר תבניות התאמה מפורט התחביר של המחרוזות האלה, ובמאמר תבניות התאמה ו-glob מוסבר איך להחריג כתובות URL.
css מערך של מחרוזות אופציונלי. רשימת קובצי ה-CSS שיוזרקו לדפים תואמים. התגיות האלה מוזרקות לפי הסדר שבו הן מופיעות במערך הזה, לפני שנוצר או מוצג DOM כלשהו בדף.
js array of strings אופציונלי. רשימת קובצי JavaScript שיוזרקו לדפים תואמים. קבצים מוזרקים לפי הסדר שבו הם מופיעים במערך הזה. כל מחרוזת ברשימה הזו צריכה להכיל נתיב יחסי למשאב בתיקיית השורש של התוסף. התווים הנטויים הקדמיים (‎/‎) נחתכים אוטומטית.
run_at RunAt אופציונלי. מציינת מתי הסקריפט צריך להיות מוחדר לדף. ברירת המחדל היא document_idle.
match_about_blank בוליאני אופציונלי. האם הסקריפט צריך להזריק למסגרת about:blank שבה מסגרת ההורה או מסגרת הפתיחה תואמת לאחד מהדפוסים שהוצהרו ב-matches. ברירת המחדל היא False.
match_origin_as_fallback בוליאני אופציונלי. האם הסקריפט צריך להזריק למסגרות שנוצרו על ידי מקור תואם, אבל כתובת ה-URL או המקור שלהן לא בהכרח תואמים ישירות לתבנית. הם כוללים מסגרות עם סכימות שונות, כמו about:, data:, blob: ו-filesystem:. כדאי לעיין גם במאמר בנושא הוספה של תגי iframe למסגרות קשורות.
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(), ולא הפונקציה המקורית עצמה. לכן, גוף הפונקציה צריך להיות עצמאי. הפניות למשתנים מחוץ לפונקציה יגרמו לסקריפט התוכן להקפיץ הודעת שגיאה 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" ],
  });
});

החרגה של התאמות ושל תבניות glob

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

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

לדוגמה, התבנית 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 או המקור של ההורה, כמו ב-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/*" ]
   }
 ],
 ...
}

Content Security Policy

לסקריפטים של תוכן שפועלים בעולמות מבודדים יש את מדיניות אבטחת התוכן (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(), חשוב לסנן את התוכן מפני מתקפות פרצת אבטחה XSS‏ (cross-site scripting) לפני שמזריקים אותו. התקשורת צריכה להתבצע רק באמצעות HTTPS כדי להימנע מהתקפות "man-in-the-middle".

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