העברת ההודעה

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

התקשורת הזו משתמשת בהעברת הודעות, שמאפשרת לתוספים ולסקריפטים של התוכן להקשיב להודעות אחד של השני ולהשיב באותו ערוץ. הודעה יכולה להכיל כל אובייקט JSON חוקי (null, בוליאני, מספר, מחרוזת, מערך או אובייקט). יש שני ממשקי API להעברת הודעות: אחד לבקשות חד-פעמיות, ואחד מורכב יותר לחיבורים לטווח ארוך שמאפשרים לשלוח כמה הודעות. מידע נוסף על שליחת הודעות בין תוספים זמין בקטע הודעות בין תוספים.

בקשות חד-פעמיות

כדי לשלוח הודעה בודדת לחלק אחר של התוסף, ואולי גם לקבל תשובה, תוכלו להתקשר למספר runtime.sendMessage() או tabs.sendMessage(). בעזרת ה-methods האלה ניתן לשלוח הודעה חד-פעמית של JSON שניתן להריץ אותה מסקריפט של תוכן, או מהתוסף לסקריפט של תוכן. כדי לטפל בתגובה, משתמשים בהבטחה שהוחזרה. כדי לשמור על תאימות לאחור עם תוספים ישנים יותר, אפשר במקום זאת להעביר את הקריאה החוזרת כארגומנט האחרון. אי אפשר להשתמש ב-promise וב-callback באותה קריאה.

מידע על המרת פונקציות קריאה חוזרת (callbacks) להבטחות (promises) ועל שימוש בהן בתוספים זמין במדריך ההעברה למניפסט V3.

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

content-script.js:

(async () => {
  const response = await chrome.runtime.sendMessage({greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

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

כדי לשלוח בקשה לסקריפט תוכן, צריך לציין את הכרטיסייה שאליה הבקשה רלוונטית, כפי שמתואר בהמשך. הדוגמה הזו פועלת בקובצי שירות (service workers), בחלונות קופצים ובדפים מסוג chrome-extension:// שנפתחים ככרטיסייה.

(async () => {
  const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
  const response = await chrome.tabs.sendMessage(tab.id, {greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

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

content-script.js או service-worker.js:

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    console.log(sender.tab ?
                "from a content script:" + sender.tab.url :
                "from the extension");
    if (request.greeting === "hello")
      sendResponse({farewell: "goodbye"});
  }
);

בדוגמה הקודמת, הפונקציה sendResponse() הוזמנה באופן סינכרוני. כדי להשתמש ב-sendResponse() באופן אסינכרוני, מוסיפים את return true; לבורר האירועים onMessage.

אם כמה דפים מקשיבים לאירועי onMessage, רק הדף הראשון שיקרא ל-sendResponse() לגבי אירוע מסוים יצליח לשלוח את התשובה. כל התגובות האחרות לאירוע הזה יידחו.

חיבורים לטווח ארוך

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

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

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

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

content-script.js:

var port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
  if (msg.question === "Who's there?")
    port.postMessage({answer: "Madame"});
  else if (msg.question === "Madame who?")
    port.postMessage({answer: "Madame... Bovary"});
});

כדי לשלוח בקשה מהתוסף לסקריפט תוכן, מחליפים את הקריאה ל-runtime.connect() בדוגמה הקודמת ב-tabs.connect().

כדי לטפל בחיבורים נכנסים עבור סקריפט תוכן או דף תוסף, מגדירים האזנה לאירועים runtime.onConnect. כשחלק אחר של התוסף קורא ל-connect(), הוא מפעיל את האירוע הזה ואת האובייקט runtime.Port. הקוד לתגובה לחיבורים נכנסים נראה כך:

service-worker.js:

chrome.runtime.onConnect.addListener(function(port) {
  console.assert(port.name === "knockknock");
  port.onMessage.addListener(function(msg) {
    if (msg.joke === "Knock knock")
      port.postMessage({question: "Who's there?"});
    else if (msg.answer === "Madame")
      port.postMessage({question: "Madame who?"});
    else if (msg.answer === "Madame... Bovary")
      port.postMessage({question: "I don't get it."});
  });
});

משך החיים של היציאה

יציאות תוכננו כשיטת תקשורת דו-כיוונית בין חלקים שונים של התוסף. מסגרת ברמה העליונה היא החלק הקטן ביותר בתוסף שיכול להשתמש ביציאה. כשחלק מהתוסף קורא ל-tabs.connect(), ל-runtime.connect() או ל-runtime.connectNative(), הוא יוצר Port שיכול לשלוח הודעות באופן מיידי באמצעות postMessage().

אם בכרטיסייה יש מספר פריימים, קריאה ל-tabs.connect() מפעילה את האירוע runtime.onConnect פעם אחת לכל פריים בכרטיסייה. באופן דומה, אם runtime.connect() נקרא, האירוע onConnect יכול להתרחש פעם לכל פריים בתהליך התוסף.

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

  • אין מאזינים של runtime.onConnect בצד השני.
  • הכרטיסייה שמכילה את השקע פורקת (לדוגמה, אם עוברים לכרטיסייה אחרת).
  • המסגרת שבה connect() הוזמנה פורקה.
  • הטעינה של כל הפריימים שקיבלו את השקע (דרך runtime.onConnect) הוסרה.
  • הצד השני מבצע קריאה אל runtime.Port.disconnect(). אם קריאה ל-connect() יוצרת כמה יציאות בצד המקבל, ו-disconnect() נקראת באחת מהיציאות האלה, האירוע onDisconnect יופעל רק ביציאה לשליחה, ולא ביציאות האחרות.

העברת הודעות בין תוספים

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

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

service-worker.js

// For a single request:
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id === blocklistedExtension)
      return;  // don't allow this extension access
    else if (request.getTargetData)
      sendResponse({targetData: targetData});
    else if (request.activateLasers) {
      var success = activateLasers();
      sendResponse({activateLasers: success});
    }
  });

// For long-lived connections:
chrome.runtime.onConnectExternal.addListener(function(port) {
  port.onMessage.addListener(function(msg) {
    // See other examples for sample onMessage handlers.
  });
});

כדי לשלוח הודעה לתוסף אחר, מעבירים את המזהה של התוסף שאליו רוצים לשלוח את ההודעה באופן הבא:

service-worker.js

// The ID of the extension we want to talk to.
var laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// For a simple request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
  }
);

// For a long-lived connection:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

שליחת הודעות מדפי אינטרנט

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

manifest.json

"externally_connectable": {
  "matches": ["https://*.example.com/*"]
}

כך תוכלו לחשוף את messaging API לכל דף שתואמת לתבניות כתובות ה-URL שציינתם. תבנית ה-URL חייבת לכלול לפחות דומיין ברמה שנייה. כלומר, לא נתמכות תבניות של שמות מארח כמו ‎*‎,‏ ‎*.com‎,‏ ‎*.co.il‎ ו-‎*.appspot.com‎. החל מגרסת Chrome 107, אפשר להשתמש ב-<all_urls> כדי לגשת לכל הדומיינים. חשוב לזכור: מכיוון שהיא משפיעה על כל המארחים, יכול להיות שהבדיקה של תוספים שמשתמשים בה בחנות האינטרנט של Chrome תימשך זמן רב יותר.

אפשר להשתמש בממשקי ה-API של runtime.sendMessage() או runtime.connect() כדי לשלוח הודעה לאפליקציה או לתוסף ספציפיים. לדוגמה:

webpage.js

// The ID of the extension we want to talk to.
var editorExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// Make a simple request:
chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
  function(response) {
    if (!response.success)
      handleError(url);
  });

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

service-worker.js

chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.url === blocklistedWebsite)
      return;  // don't allow this web page access
    if (request.openUrlInEditor)
      openUrl(request.openUrlInEditor);
  });

העברת הודעות מקומית

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

שיקולי אבטחה

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

סקריפטים של תוכן הם פחות מהימנים

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

פרצת אבטחה XSS‏ (cross-site scripting)

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

שיטות בטוחות יותר

שימוש בממשקי API שלא מריצים סקריפטים כשהדבר אפשרי:

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // JSON.parse doesn't evaluate the attacker's scripts.
  var resp = JSON.parse(response.farewell);
});

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // innerText does not let the attacker inject HTML elements.
  document.getElementById("resp").innerText = response.farewell;
});
שיטות לא בטוחות

מומלץ להימנע מהשיטות הבאות שעלולות לגרום לפגיעה בתוסף:

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be evaluating a malicious script!
  var resp = eval(`(${response.farewell})`);
});

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be injecting a malicious script!
  document.getElementById("resp").innerHTML = response.farewell;
});