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

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

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

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

סקריפטים של תוכן לא יכולים לגשת ישירות לממשקי API אחרים. אבל הם יכולים לגשת אליהם באופן עקיף על ידי החלפת הודעות עם חלקים אחרים של התוסף.

עבודה בעולמות מבודדים

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

בעקבות שינוי זה, שתי ההתראות מופיעות ברצף כאשר לוחצים על הלחצן.

החדרת סקריפטים

אפשר להצהיר על סקריפטים של תוכן באופן סטטי, להצהיר על באופן דינמי או להחדר באופן פרוגרמטי.

החדרה עם הצהרות סטטיות

צריך להשתמש בהצהרות של סקריפט תוכן סטטי ב-מניפסט.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, ראו את המאמר דפוסי התאמה על התחביר של המחרוזות האלה ותבניות התאמה וסימני התאמה.
css מערך של מחרוזות אופציונלי. הרשימה של קובצי CSS שיש להכניס לדפים תואמים. הן מוחדרות לפי הסדר שבו הן מופיעות במערך הזה, לפני ש-DOM כלשהו נוצר או מוצג עבור הדף.
js מערך מחרוזות אופציונלי. רשימת קובצי JavaScript שיש להכניס לדפים תואמים. הקבצים מוחדרים לפי הסדר שבו הם מופיעים במערך הזה. כל מחרוזת ברשימה הזו צריכה להכיל נתיב יחסי למשאב בספריית השורש של התוסף. קווים נטויים מובילים (`/`) חתוכים באופן אוטומטי.
run_at RunAt אופציונלי. מציין מתי יש להחדיר את הסקריפט לדף. ברירת המחדל היא document_idle.
match_about_blank boolean אופציונלי. האם הסקריפט צריך להחדיר מסגרת about:blank שבה מסגרת ההורה או מסגרת הפתיחה תואמות לאחת מהתבניות שהוצהרו ב-matches. ברירת המחדל היא FALSE.
match_origin_as_fallback boolean אופציונלי. האם הסקריפט צריך להחדיר פריימים שנוצרו על ידי מקור תואם, אבל יכול להיות שכתובת ה-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" באופן זמני.

בהמשך מופיעות גרסאות שונות של תוסף מבוסס-Tab פעיל.

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" ],
  });
});

החרגה של התאמות וגלובוסים

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

שם סוג תיאור
exclude_matches מערך של מחרוזות אופציונלי. לא כולל דפים שסקריפט התוכן הזה היה מוחדר אליהם באופן אחר. למידע נוסף על התחביר של מחרוזות אלה, ראו תבניות התאמה.
include_globs מערך של מחרוזות אופציונלי. הוחל אחרי matches כדי לכלול רק את כתובות ה-URL שתואמות גם לדומיין הזה. הטקסט נועד לחקות את מילת המפתח @include Garemonkey.
exclude_globs מערך של מחרוזת אופציונלי. הוחל אחרי matches כדי להחריג כתובות URL שתואמות לכדור הארץ הזה. נועד לחקות את מילת המפתח @exclude Graeasemonkey.

סקריפט התוכן יוחדר לדף אם שני התנאים הבאים מתקיימים:

  • כתובת ה-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"]
    }
  ],
  ...
}

service-worker.js

chrome.scripting.registerContentScripts([{
  id: "test",
  matches : [ "https://*.nytimes.com/*" ],
  allFrames : true,
  js : [ "contentScript.js" ],
}]);
שם סוג תיאור
all_frames boolean אופציונלי. ערך ברירת המחדל הוא false, כלומר רק המסגרת העליונה תואמת.

אם צוין true, כל הפריימים יוחדרו אליו, גם אם הפריים הוא לא המסגרת העליונה בכרטיסייה. כל מסגרת נבדקת בנפרד כדי לראות את הדרישות לגבי כתובות ה-URL. היא לא תוחדר למסגרות צאצא אם הן לא עומדות בדרישות של כתובות ה-URL.

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

מי שיצר את המסגרת הוא המסגרת שיצרה את פריים היעד או ניווטה בו. בדרך כלל מדובר בהורה הישיר או בפתיחה, אבל לא בטוח שהוא יהיה זה (כמו במקרה של מסגרת שמנווטת ב-iframe בתוך iframe).

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

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