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

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

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

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

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

החדרה באמצעות הצהרות סטטיות

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

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

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

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

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 בוליאני אופציונלי. ערך ברירת המחדל הוא false. כלומר, רק המסגרת העליונה תואמת.

אם מצוין true, כל הפריימים יוחדרו אליהן, גם אם היא לא המסגרת העליונה בכרטיסייה. כל מסגרת נבדקת בנפרד לכתובת URL בדרישות שלנו. אם כתובת ה-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 של המסגרת עצמה. לתשומת ליבכם: המזהה הזה עשוי להיות גם שונה origin של מסגרת היעד (למשל, ל-data: כתובות URL יש מקור 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 כדי להימנע מהתקפות &quot;man-in-the-middle&quot;.

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