מאחר שסקריפטים של תוכן פועלים בהקשר של דף אינטרנט, ולא בהקשר של התוסף שמפעיל אותם, לעיתים קרובות הם זקוקים לדרכים לתקשר עם שאר התוסף. לדוגמה, תוסף של קורא RSS עשוי להשתמש בסקריפטים של תוכן כדי לזהות את נוכחותו של פיד RSS בדף, ואז להודיע ל-service worker להציג סמל פעולה בדף הזה.
התקשורת הזו מתבצעת באמצעות העברת הודעות, שמאפשרת גם לתוספים וגם לסקריפטים של תוכן להאזין להודעות של זה לזה ולהשיב באותו ערוץ. הודעה יכולה להכיל כל אובייקט JSON חוקי (null, בוליאני, מספר, מחרוזת, מערך או אובייקט). יש שני ממשקי API להעברת הודעות: אחד לבקשות חד-פעמיות, ואחד מורכב יותר לחיבורים לטווח ארוך שמאפשרים לשלוח כמה הודעות. מידע נוסף על שליחת הודעות בין תוספים זמין בקטע הודעות בין תוספים.
בקשות חד-פעמיות
כדי לשלוח הודעה אחת לחלק אחר בתוסף, ולקבל תשובה אם רוצים, אפשר להתקשר למספר runtime.sendMessage()
או למספר tabs.sendMessage()
.
השיטות האלה מאפשרות לשלוח הודעה חד-פעמית של JSON שניתן לסריאליזציה מסקריפט תוכן לתוסף, או מהתוסף לסקריפט תוכן. כדי לטפל בתגובה, משתמשים בהבטחה שהוחזרה. כדי לשמור על תאימות לאחור עם תוספים ישנים יותר, אפשר להעביר במקום זאת קריאה חוזרת (callback) בתור הארגומנט האחרון. אי אפשר להשתמש ב-promise וב-callback באותה קריאה.
כששולחים הודעה, למעקב האירועים שמטפל בהודעה מועבר ארגומנטים שלישי אופציונלי, sendResponse
. זוהי פונקציה שמקבלת אובייקט שאפשר ליצור ממנו סדרה של ערכים ב-JSON, ומשתמשת בו כערך המוחזר לפונקציה ששלחה את ההודעה. כברירת מחדל, צריך להפעיל את הפונקציה הלא סטריאוטיפית sendResponse
באופן סינכרוני. אם רוצים לבצע עבודה אסינכרונית כדי לקבל את הערך שמוענק ל-sendResponse
, צריך להחזיר ליטרל true
(ולא רק ערך נכון) ממאזין האירועים. הפעולה הזו תשאיר את ערוץ ההודעות פתוח לקצה השני עד שיקראו ל-sendResponse
.
// Event listener
function handleMessages(message, sender, sendResponse) {
fetch(message.url)
.then((response) => sendResponse({statusCode: response.status}))
// Since `fetch` is asynchronous, must send an explicit `true`
return true;
}
// Message sender
const {statusCode} = await chrome.runtime.sendMessage({
url: 'https://example.com'
});
מידע על המרת פונקציות קריאה חוזרת (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
כדי לשמור את פונקציית ה-call back 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()
, הוא יוצר יציאה שיכולה לשלוח הודעות באופן מיידי באמצעות 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 === 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 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:
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.
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);
});
העברת הודעות מקומית
תוספים יכולים להחליף הודעות עם אפליקציות מקוריות שרשומים בתור מארחי הודעות מקומיים. מידע נוסף על התכונה הזו זמין במאמר הודעות מוטמעות.
שיקולי אבטחה
ריכזנו כאן כמה שיקולים בנושא אבטחה שקשורים לשליחת הודעות.
סקריפטים של תוכן פחות מהימנים
סקריפטים של תוכן פחות מהימנים מאשר ה-service worker של התוסף. לדוגמה, דף אינטרנט זדוני עלול לפגוע בתהליך העיבוד (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; });