תסריטי תוכן

תסריט תוכן הוא חלק מההרחבה שלך שרץ בהקשר של דף מסויים ברשת  (בניגוד לתסריטי רקע שהם חלק מהרחבה, או תסריטים שהם חלק מאתר הרשת עצמו , כגון אלה הנטענים באמצעות אלמנט ה-<script> ).

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

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

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

לתשומת לבך תסריטי תוכן חסומים במתחמים הבאים:

  • accounts-static.cdn.mozilla.net
  • accounts.firefox.com
  • addons.cdn.mozilla.net
  • addons.mozilla.org
  • api.accounts.firefox.com
  • content.cdn.mozilla.net
  • content.cdn.mozilla.net
  • discovery.addons.mozilla.org
  • input.mozilla.org
  • install.mozilla.org
  • oauth.accounts.firefox.com
  • profile.accounts.firefox.com
  • support.mozilla.org
  • sync.services.mozilla.com
  • testpilot.firefox.com

נסיון להחדיר תסריט תוכן לאתרים אלה ייכשל טהעמוד ירשום שגיאת  CSP ללוג.

משום שמגבלות אלו כוללות את  addons.mozilla.org, משתמשים עשויים לנסות להשתמש בהרחבה שלך מיד אחרי ההתקנה —רק כדי לגלות שאינה עובדת! ייתכן ותרצה/י להוסיף אזהרה הולמת, או   דף onboarding להרחיק משתמשים מ- addons.mozilla.org.

ערכים הנוספים לסקופ הגלובלי של תסריט תוכן באמצעות var foo או window.foo = "bar" עלולים להיעלם עקב באג 1408996.

טעינת תסריט תוכן

ניתן לטעון תסריט תוכן לאתר רשת בשלוש דרכים:

  1. בעת ההתקנה, לתוך דפים התואמים תבניות URL: באמצעות מפתח ה-content_scripts בקובץ ה-manifest.json שלך, ניתן  לבקש מהדפדפן לטעון תסריט תוכן בכל פעם שהדפדפן מעלה דף שה-URL שלו תואם תבנית נתונה.
  2. בזמן ריצה, לתוך דפים התואמים תבניתו URL : באמצעות ממשק פיתוח היישומים contentScripts API, ניתן לבקש מהדפדפן לטעון  תסריט תוכן בכל ]עפ שהדפדפן טוען דף שה-URL שלו  תואם תבנית נתונה matches a given pattern. זה בפשטות כמו בשיטה  (1), פרט לכך שניתן להוסיף ולהסיר תסריט תוכן בזמן ריצה.
  3. בזמן ריצה, לתוך לשוניות מסוימות  into specific tabs: באמצאות צצשק הפיתוח  tabs.executeScript() , ניתן לטעון תסריט תוכן בכל עת שיהיה ברצונך: למשל, סתגובה להקשת משתמש/ת על פעולת דפדפן.

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

באמצעות שיטות (1) ו- (2), ניתן לטעון תסריטים לתוך דפים שה- URL-ים שלהם  יכולים להיות מיוצגים באמצעות תבניות התאמה.

באמצעות שיטה (3), ניתן גם לטעון תסריטים לתוך דפים ארוזים עם ההרחבה שלך, אך לא ניתן לטעון תסריטים לתוך דפי דפדפן בעלי פריבילגיה  (כגון "about:debugging" או "about:addons").

סביבת תסריטי תוכן

גישה ל-DOM

תסריטים יכולים לגשת ולשנות את ה-DOM של הדף , בדיוק כפי שיכולים לעשות זת תסריטי דף נורמליים. הם יכולים לראות גם שינווים שנעשו ב- DOM על ידי תסריטי דפים.

אולם, תסריטי תוכן מקבלים "מראה נקי של ה-DOM". כלומר::

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

בפיירפוקס התנהגות זו קרויה ראיית רנטגן.

חשבו על דף רשת כזה:

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  </head>

  <body>
    <script src="page-scripts/page-script.js"></script>
  </body>
</html>

התסריט "page-script.js" עושה את זה:

// page-script.js

// add a new element to the DOM
var p = document.createElement("p");
p.textContent = "This paragraph was added by a page script.";
p.setAttribute("id", "page-script-para");
document.body.appendChild(p);

// define a new property on the window
window.foo = "This global variable was added by a page script";

// redefine the built-in window.confirm() function
window.confirm = function() {
  alert("The page script has also redefined 'confirm'");
}

כעת  הרחבה מחדירה תסריט תוכן לתוך הדף:

// content-script.js

// can access and modify the DOM
var pageScriptPara = document.getElementById("page-script-para");
pageScriptPara.style.backgroundColor = "blue";

// can't see page-script-added properties
console.log(window.foo);  // undefined

// sees the original form of redefined properties
window.confirm("Are you sure?"); // calls the original window.confirm()

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

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

אחת ההשלכות המעשיות של התנהגות זו היא שלתסריט תוכן לא תהיה גישה לאף ספריית ד'אווהסקריפט שנטענה על ידי הדף. לכן לדוגמא, אם דף מכיל jQuery, תסריט התוכן לא יוכל לראותו.

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

"content_scripts": [
  {
    "matches": ["*://*.mozilla.org/*"],
    "js": ["jquery.js", "content-script.js"]
  }
]

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

ראו  שיתוף אובייקטים עם תסריטי דף לפרטים נוספים.

ממשקי פיתוח יישומים של הרחבות רשת

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

מתוך extension:

מתוך runtime:

מתוך i18n:

מתוך menus:

הכל מתוך storage.

XHR ו-Fetch

.תסריטי תוכן יכולים להכין בקשות באמצעות ממשקי פיתוח היישומים window.XMLHttpRequest ו- window.fetch().

בפיירפוקס, בקשות תסריטי התוכן (לדוגמא, באמצעות fetch()) קורות בהקשר של הרחבה, כך שיש לספק כתובת  URL מוחלטת  כדי להתייחס לתוכן דף. בכרום, בקשות אלה קורות בהקשר של דף, כך שכתובות ה-URL הן יחסיות,  לדוגמא, /api נשלחת ל- https://[current page URL]/api.

תסריטי תוכן מקבלים את אותן פריבילגיות חוצות מתחמים כמו שאר ההרחבות, כך שאם הרחבה בקשה גישה חוצת מתחמים עבור מתחם באמצעות מפתח  permissions ב- manifest.json, אזי תסריט התוכן יקבל גישה גם למתחם זה. 

זה יושג על ידי חשיפת יותר מופעי   XHR ו- fetch  בעלי יותר פריבילגיות בתסריט התוכן, להם תופעת הלוואי של אי הגדרת כותרות הorigin והreferer כמו לבקשות מהדף עצמו, לעתים תכופות זה מועדף כדי למנוע מבקשה לחשוף את הטבע חוצה המקורות שלה.  מגרסה 58 ואילך  הרחבות הצריכות לבצע בקשות המתנהגות כאילו משלחו מהתוכן עצמו  יכולות להשתמש ב- content.XMLHttpRequest וב-content.fetch() במקום. להרחבות חוצות דפדפנים נוכחותן חייבת להיו מגולת תכונות.

תקשורת עם תסריטי רקע

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

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

הודעות בודדות

לשליחת הודעה בודדת עם אפשרות למענה, ניתן להשתמש בממשקי הפיתוח הבאים:

בתסריט התוכן בתסריט הרקע
שליחת הודעה browser.runtime.sendMessage() browser.tabs.sendMessage()
קבלת הודעה browser.runtime.onMessage browser.runtime.onMessage

לדוגמא, להלן תסריט תוכן המאזין לאירועי להקלקה בדף הרשת.

עם ההקלקה הייתה על קישור, יישלח URL המטרה לדף הרקע:

// content-script.js

window.addEventListener("click", notifyExtension);

function notifyExtension(e) {
  if (e.target.tagName != "A") {
    return;
  }
  browser.runtime.sendMessage({"url": e.target.href});
}

תסריט הרקע מאזין להודעות האלה ומציג התראה באמצעוץ ממשק ה-notifications :

// background-script.js

browser.runtime.onMessage.addListener(notify);

function notify(message) {
  browser.notifications.create({
    "type": "basic",
    "iconUrl": browser.extension.getURL("link.png"),
    "title": "You clicked a link!",
    "message": message.url
  });
}

(הקוד לדוגמא מעובד קלות מהדוגמא notify-link-clicks-i18n   ב-GitHub.)

טיפול בהודעות מבוסס התחברות

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

לכל צד יש אובייקט runtime.Port, המשמש החלפת הודעות.

ליצירת התחברות:

זה מחזיר אובייקט מסוג runtime.Port.

ברגע שלכל צד יש פתחה , שני הצדדים יכולים:

  • לשלוח הודעות באמצעות runtime.Port.postMessage(), וגם
  • לקבל הודעות באמצעות runtime.Port.onMessage()

לדוגמא, מייד כשנטען, תסריט התוכן:

  • מתחבר לתסריט הרקע
  • מאחסן את ה-Port במשתנה myPort
  • מאזין להודעות על גבי myPort, ורושם אותן ליומן
  • משתמש ב-myPort לשלוח הודעות לתסריט הרקע  כשהמתמש/ת מקליק/ה על המסמך
// content-script.js

var myPort = browser.runtime.connect({name:"port-from-cs"});
myPort.postMessage({greeting: "hello from content script"});

myPort.onMessage.addListener(function(m) {
  console.log("In content script, received message from background script: ");
  console.log(m.greeting);
});

document.body.addEventListener("click", function() {
  myPort.postMessage({greeting: "they clicked the page!"});
});

תסריט הרקע המכותב:

  • מאזין לנסיונות ההתחברות של תסריט התוכן
  • כשמקבל נסיון התחברות:
    • שומר את הפתחה במשתנה בשם portFromCS
    • שולח לתסריט התוכן הודעה באמצעות הפתחה
    • מתחיל להאזין להודעות המתקבלות על גבי הפתחה, ורושם אותן ליומן
  • שולח הודעות לתסריט התוכן, using portFromCS, כשהמשתמש/ת מקליק/ה על פעולת הדפדפן של ההרחבה.
// background-script.js

var portFromCS;

function connected(p) {
  portFromCS = p;
  portFromCS.postMessage({greeting: "hi there content script!"});
  portFromCS.onMessage.addListener(function(m) {
    console.log("In background script, received message from content script");
    console.log(m.greeting);
  });
}

browser.runtime.onConnect.addListener(connected);

browser.browserAction.onClicked.addListener(function() {
  portFromCS.postMessage({greeting: "they clicked the button!"});
});

תסריטי תוכן מרובים

אם יש לך תסריטי תוכן מרובים המתקשרים בו זמנית, ייתכן שתרצה/י לאחסן כל התחברות במערך.

// background-script.js

var ports = []

function connected(p) {
  ports[p.sender.tab.id]    = p
  //...
}

browser.runtime.onConnect.addListener(connected)

browser.browserAction.onClicked.addListener(function() {
  ports.forEach(p => {
        p.postMessage({greeting: "they clicked the button!"})
    })
});

תקשורת עם דף רשת

למרות שכברירת מחדל תסריט תוכן אינם מקבלים הרשאת גישה לאובייקטים שנוצרו על ידי תסריטי דף, הם יכולים להתקשר עם תסריטי דף באמצעות ממשקי ה-DOM  window.postMessage ו-window.addEventListener .

לדוגמא:

// page-script.js

var messenger = document.getElementById("from-page-script");

messenger.addEventListener("click", messageContentScript);

function messageContentScript() {
  window.postMessage({
    direction: "from-page-script",
    message: "Message from the page"
  }, "*");
// content-script.js

window.addEventListener("message", function(event) {
  if (event.source == window &&
      event.data &&
      event.data.direction == "from-page-script") {
    alert("Content script received message: \"" + event.data.message + "\"");
  }
});

לדוגמא שלמה עובדת של זה, בקרו בדף ההדגמה של- GitHub ובצעו את ההוראות

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

לצורך דוגמא טריוויאלית, הניחו כי קוד של תסריט תוכן שמקבל את ההודעה עושה דבר כזה:

// content-script.js

window.addEventListener("message", function(event) {
  if (event.source == window &&
      event.data.direction &&
      event.data.direction == "from-page-script") {
    eval(event.data.message);
  }
});

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

שימוש ב- eval() בתסריטי תוכן

בכרום, eval() תמיד מריץ קוד בהקשר של תסריט התוכן, לא בהקשר של הדף.

בפיירפוקס:

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

לדוגמא, ניקח תסריט תוכן כזה:

// content-script.js

window.eval('window.x = 1;');
eval('window.y = 2');

console.log(`In content script, window.x: ${window.x}`);
console.log(`In content script, window.y: ${window.y}`);

window.postMessage({
  message: "check"
}, "*");

הקוד רק יוצר משתנים x ו- y באמצעות window.eval() ו-eval(), ואז רושם את ערכיהם ליומן, ואז שולח הודעה לדף.

בהתקבל ההודעה, תסריט הדף רושם ליומן את אותם המשתנים:

window.addEventListener("message", function(event) {
  if (event.source === window && event.data && event.data.message === "check") {
    console.log(`In page script, window.x: ${window.x}`);
    console.log(`In page script, window.y: ${window.y}`);
  }
});

בכרום, מופק פלט כזה:

In content script, window.x: 1
In content script, window.y: 2
In page script, window.x: undefined
In page script, window.y: undefined

בפיירפוקס מופק הפלט הבא:

In content script, window.x: undefined
In content script, window.y: 2
In page script, window.x: 1
In page script, window.y: undefined

אותו הדבר תקף עבור setTimeout(), setInterval(), ו- Function().

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

// page.js redefines console.log

var original = console.log;

console.log = function() {
  original(true);
}
 
// content-script.js calls the redefined version

window.eval('console.log(false)');