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

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

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

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