העברת ההודעה

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

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

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

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

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

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

כדי לטפל בחיבורים נכנסים לסקריפט תוכן או לדף של תוסף, יש להגדיר פונקציות event listener ל-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(), נוצרת יציאה שיכולה לשלוח הודעות באופן מיידי באמצעות 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/*"]
}

הפעולה הזו חושפת את ה-API להעברת הודעות לכל דף שתואם לתבניות ה-URL שציינתם. תבנית ה-URL צריכה להכיל לפחות דומיין ברמה השנייה. כלומר, אין תמיכה בתבניות של שמות מארחים כמו "*", "*.com" , "*.co.uk" ו-"*.appspot.com". החל מגרסה 107 של Chrome, תוכלו להשתמש ב-<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);
  });

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

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

שיקולי אבטחה

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

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

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

סקריפטים חוצי-אתרים

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