העברת ההודעה

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

יש שני ממשקי API להעברת הודעות: אחד לבקשות חד-פעמיות, ומורכב יותר לחיבורים לטווח ארוך שמאפשרים לשלוח כמה הודעות.

מידע על שליחת הודעות בין תוספים זמין בקטע הודעות בין תוספים.

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

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

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

content-script.js:

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

תשובות

כדי להאזין להודעה, משתמשים באירוע chrome.runtime.onMessage:

// Event listener
function handleMessages(message, sender, sendResponse) {
  fetch(message.url)
    .then((response) => sendResponse({statusCode: response.status}))

  // Since `fetch` is asynchronous, must return an explicit `true`
  return true;
}

chrome.runtime.onMessage.addListener(handleMessages);

// From the sender's context...
const {statusCode} = await chrome.runtime.sendMessage({
  url: 'https://example.com'
});

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

אם מתקשרים אל sendResponse בלי פרמטרים, נשלחת התגובה null.

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

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

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

  • runtime.connect() כדי להעביר הודעות מסקריפט תוכן לדף תוסף
  • tabs.connect() כדי להעביר הודעות מדף תוסף לסקריפט תוכן.

אפשר לתת שם לערוץ על ידי העברת פרמטר של אפשרויות עם מפתח name כדי להבחין בין סוגים שונים של חיבורים:

const port = chrome.runtime.connect({name: "example"});

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

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

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

content-script.js:

const port = chrome.runtime.connect({name: "knockknock"});
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"});
  }
});
port.postMessage({joke: "Knock knock"});

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

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

service-worker.js:

chrome.runtime.onConnect.addListener(function(port) {
  if (port.name !== "knockknock") {
    return;
  }
  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."});
    }
  });
});

סידור הפרקים

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

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

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

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

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

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

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

service-worker.js

// For a single request:
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id !== allowlistedExtension) {
      return; // don't allow this extension access
    }
    if (request.getTargetData) {
      sendResponse({ targetData: targetData });
    } else if (request.activateLasers) {
      const 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.
const laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

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

// For a long-lived connection:
const 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'. אפשר להשתמש ב-<all_urls> כדי לגשת לכל הדומיינים.

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

webpage.js

// The ID of the extension we want to talk to.
const editorExtensionId = 'abcdefghijklmnoabcdefhijklmnoabc';

// Check if extension is installed
if (chrome && chrome.runtime) {
  // Make a request:
  chrome.runtime.sendMessage(
    editorExtensionId,
    {
      openUrlInEditor: url
    },
    (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);
  });

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

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

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

שיקולי אבטחה

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

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

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

פרצת אבטחה 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.
  const 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!
  const 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;
});