סקריפטים של תוכן הם קבצים שפועלים בהקשר של דפי אינטרנט. באמצעות Document Object Model (מודל אובייקט מסמך) סטנדרטי, הם יכולים לקרוא פרטים של דפי האינטרנט שהדפדפן מבקר בהם, לבצע בהם שינויים ולהעביר מידע לתוסף האב שלהם.
הסבר על היכולות של סקריפטים של תוכן
סקריפטים של תוכן יכולים לגשת ישירות לממשקי ה-API הבאים של התוסף:
dom
i18n
storage
runtime.connect()
runtime.getManifest()
runtime.getURL()
runtime.id
runtime.onConnect
runtime.onMessage
runtime.sendMessage()
סקריפטים של תוכן לא יכולים לגשת ישירות לממשקי API אחרים. אבל הם יכולים לגשת אליהם באופן עקיף על ידי החלפת הודעות עם חלקים אחרים של התוסף.
אפשר גם לגשת לקבצים אחרים בתוסף מסקריפט תוכן, באמצעות ממשקי API כמו fetch()
. כדי לעשות את זה, צריך להגדיר אותם כמשאבים שאפשר לגשת אליהם דרך האינטרנט. חשוב לשים לב שהפעולה הזו חושפת את המשאבים לכל סקריפט של צד ראשון או צד שלישי שפועל באותו אתר.
עבודה בעולמות מבודדים
סקריפטים של תוכן פועלים בסביבה מבודדת, כך שסקריפט תוכן יכול לבצע שינויים בסביבת JavaScript שלו בלי להתנגש עם הדף או עם סקריפטים של תוכן של תוספים אחרים.
תוסף יכול לפעול בדף אינטרנט עם קוד שדומה לדוגמה הבאה.
webPage.html
<html>
<button id="mybutton">click me</button>
<script>
var greeting = "hello, ";
var button = document.getElementById("mybutton");
button.person_name = "Bob";
button.addEventListener(
"click", () => alert(greeting + button.person_name + "."), false);
</script>
</html>
התוסף הזה יכול להחדיר את סקריפט התוכן הבא באמצעות אחת מהשיטות שמפורטות בקטע החדרת סקריפטים.
content-script.js
var greeting = "hola, ";
var button = document.getElementById("mybutton");
button.person_name = "Roberto";
button.addEventListener(
"click", () => alert(greeting + button.person_name + "."), false);
בעקבות השינוי, שתי ההתראות מופיעות ברצף כשלוחצים על הלחצן.
החדרת סקריפטים
אפשר להצהיר על סקריפטים של תוכן באופן סטטי, להצהיר עליהם באופן דינמי או להחדיר אותם באופן פרוגרמטי.
הוספה באמצעות הצהרות סטטיות
משתמשים בהצהרות סטטיות של סקריפטים של תוכן בקובץ manifest.json עבור סקריפטים שצריכים לפעול באופן אוטומטי בקבוצה מוכרת של דפים.
סקריפטים שהוצהרו באופן סטטי רשומים במניפסט תחת המפתח "content_scripts"
.
הם יכולים לכלול קובצי JavaScript, קובצי CSS או את שניהם. בכל הסקריפטים של תוכן שמופעלים אוטומטית צריך לציין תבניות התאמה.
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"css": ["my-styles.css"],
"js": ["content-script.js"]
}
],
...
}
שם | סוג | תיאור |
---|---|---|
matches |
מערך של מחרוזות | חובה. מציין באילו דפים יוזרק סקריפט התוכן הזה. במאמר תבניות התאמה מפורט התחביר של המחרוזות האלה, ובמאמר תבניות התאמה ו-glob מוסבר איך להחריג כתובות URL. |
css |
מערך של מחרוזות | אופציונלי. רשימת קובצי ה-CSS שיוזרקו לדפים תואמים. התגיות האלה מוזרקות לפי הסדר שבו הן מופיעות במערך הזה, לפני שנוצר או מוצג DOM כלשהו בדף. |
js |
|
אופציונלי. רשימת קובצי JavaScript שיוזרקו לדפים תואמים. קבצים מוזרקים לפי הסדר שבו הם מופיעים במערך הזה. כל מחרוזת ברשימה הזו צריכה להכיל נתיב יחסי למשאב בתיקיית השורש של התוסף. התווים הנטויים הקדמיים (/) נחתכים אוטומטית. |
run_at |
RunAt | אופציונלי. מציינת מתי הסקריפט צריך להיות מוחדר לדף. ברירת המחדל היא
document_idle . |
match_about_blank |
בוליאני | אופציונלי. האם הסקריפט צריך להזריק למסגרת about:blank שבה מסגרת ההורה או מסגרת הפתיחה תואמת לאחד מהדפוסים שהוצהרו ב-matches . ברירת המחדל היא False. |
match_origin_as_fallback |
בוליאני |
אופציונלי. האם הסקריפט צריך להזריק למסגרות שנוצרו על ידי מקור תואם, אבל כתובת ה-URL או המקור שלהן לא בהכרח תואמים ישירות לתבנית. הם כוללים מסגרות עם סכימות שונות, כמו about: , data: , blob: ו-filesystem: . כדאי לעיין גם במאמר בנושא הוספה של תגיות למסגרות קשורות.
|
world |
ExecutionWorld |
אופציונלי. הסביבה של JavaScript שבה הסקריפט יפעל. ברירת המחדל היא ISOLATED . מידע נוסף זמין במאמר בנושא עבודה בעולמות מבודדים.
|
הוספה באמצעות הצהרות דינמיות
סקריפטים דינמיים של תוכן שימושיים כשדפוסי ההתאמה של סקריפטים של תוכן לא ידועים, או כשלא תמיד צריך להחדיר סקריפטים של תוכן למארחים ידועים.
ההצהרות הדינמיות הוצגו ב-Chrome 96. הן דומות להצהרות סטטיות, אבל אובייקט סקריפט התוכן רשום ב-Chrome באמצעות שיטות במרחב השמות chrome.scripting
ולא ב-manifest.json. Scripting API גם מאפשר למפתחי תוספים:
- רישום סקריפטים של תוכן.
- הורדת רשימה של סקריפטים של תוכן רשום.
- מעדכנים את רשימת סקריפטים התוכן הרשומים.
- הסרה של סקריפטים של תוכן רשום.
בדומה להצהרות סטטיות, הצהרות דינמיות יכולות לכלול קובצי JavaScript, קובצי CSS או את שניהם.
service-worker.js
chrome.scripting
.registerContentScripts([{
id: "session-script",
js: ["content.js"],
persistAcrossSessions: false,
matches: ["*://example.com/*"],
runAt: "document_start",
}])
.then(() => console.log("registration complete"))
.catch((err) => console.warn("unexpected error", err))
service-worker.js
chrome.scripting
.updateContentScripts([{
id: "session-script",
excludeMatches: ["*://admin.example.com/*"],
}])
.then(() => console.log("registration updated"));
service-worker.js
chrome.scripting
.getRegisteredContentScripts()
.then(scripts => console.log("registered content scripts", scripts));
service-worker.js
chrome.scripting
.unregisterContentScripts({ ids: ["session-script"] })
.then(() => console.log("un-registration complete"));
החדרה פרוגרמטית
משתמשים בהחדרה פרוגרמטית לסקריפטים של תוכן שצריכים לפעול בתגובה לאירועים או במקרים ספציפיים.
כדי להחדיר סקריפט תוכן באופן פרוגרמטי, התוסף צריך הרשאות מארח לדף שאליו הוא מנסה להחדיר סקריפטים. אפשר להעניק הרשאות מארח על ידי שליחת בקשה להרשאות כחלק ממניפסט התוסף, או על ידי שימוש זמני ב-"activeTab"
.
בהמשך מופיעות גרסאות שונות של תוסף שמבוסס על activeTab.
manifest.json:
{
"name": "My extension",
...
"permissions": [
"activeTab",
"scripting"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_title": "Action Button"
}
}
אפשר להחדיר סקריפטים של תוכן כקבצים.
content-script.js
document.body.style.backgroundColor = "orange";
service-worker.js:
chrome.action.onClicked.addListener((tab) => {
chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ["content-script.js"]
});
});
לחלופין, אפשר להחדיר גוף של פונקציה ולהריץ אותו כסקריפט תוכן.
service-worker.js:
function injectedFunction() {
document.body.style.backgroundColor = "orange";
}
chrome.action.onClicked.addListener((tab) => {
chrome.scripting.executeScript({
target : {tabId : tab.id},
func : injectedFunction,
});
});
חשוב לדעת שהפונקציה שמוזרקת היא עותק של הפונקציה שאליה מתייחסת הקריאה chrome.scripting.executeScript()
, ולא הפונקציה המקורית עצמה. כתוצאה מכך, גוף הפונקציה צריך להיות עצמאי. הפניות למשתנים מחוץ לפונקציה יגרמו לסקריפט התוכן להחזיר ReferenceError
.
כשמזריקים כפונקציה, אפשר גם להעביר ארגומנטים לפונקציה.
service-worker.js
function injectedFunction(color) {
document.body.style.backgroundColor = color;
}
chrome.action.onClicked.addListener((tab) => {
chrome.scripting.executeScript({
target : {tabId : tab.id},
func : injectedFunction,
args : [ "orange" ],
});
});
החרגה של התאמות ושל תבניות glob
כדי להתאים אישית את ההתאמה של דפים שצוינו, צריך לכלול את השדות הבאים בהצהרה על רישום.
שם | סוג | תיאור |
---|---|---|
exclude_matches |
מערך של מחרוזות | אופציונלי. המאפיין הזה מחריג דפים שהסקריפט הזה של התוכן היה מוזרק אליהם אחרת. פרטים על התחביר של המחרוזות האלה מופיעים במאמר בנושא דפוסי התאמה. |
include_globs |
מערך של מחרוזות | אופציונלי. המסנן מוחל אחרי matches כדי לכלול רק כתובות URL שתואמות גם ל-glob הזה. המטרה היא לחקות את מילת המפתח @include
Greasemonkey. |
exclude_globs |
array of string | אופציונלי. ההחרגה מתבצעת אחרי matches כדי להחריג כתובות URL שתואמות ל-glob הזה. התכונה הזו נועדה לחקות את מילת המפתח @exclude של Greasemonkey. |
סקריפט התוכן יוזרק לדף אם שני התנאים הבאים מתקיימים:
- כתובת ה-URL שלה תואמת לכל תבנית
matches
ולכל תבניתinclude_globs
. - כתובת ה-URL לא תואמת גם לתבנית של
exclude_matches
אוexclude_globs
. מאחר שהמאפייןmatches
הוא מאפיין חובה, אפשר להשתמש במאפייניםexclude_matches
,include_globs
ו-exclude_globs
רק כדי להגביל את הדפים שיושפעו.
התוסף הבא מחדיר את הסקריפט של התוכן אל https://www.nytimes.com/health
אבל לא אל https://www.nytimes.com/business
.
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"exclude_matches": ["*://*/*business*"],
"js": ["contentScript.js"]
}
],
...
}
service-worker.js
chrome.scripting.registerContentScripts([{
id : "test",
matches : [ "https://*.nytimes.com/*" ],
excludeMatches : [ "*://*/*business*" ],
js : [ "contentScript.js" ],
}]);
מאפייני Glob פועלים לפי תחביר שונה וגמיש יותר מדפוסי התאמה. מחרוזות glob קבילות הן כתובות URL שעשויות להכיל כוכביות וסימני שאלה שהם 'תווים כלליים לחיפוש'. הכוכבית (*
)
תואמת לכל מחרוזת בכל אורך, כולל מחרוזת ריקה, בעוד שסימן השאלה (?
) תואם
לכל תו יחיד.
לדוגמה, ה-glob https://???.example.com/foo/\*
מתאים לכל אחת מהאפשרויות הבאות:
https://www.example.com/foo/bar
https://the.example.com/foo/
עם זאת, היא לא תתאים לכתובות הבאות:
https://my.example.com/foo/bar
https://example.com/foo/
https://www.example.com/foo
התוסף הזה מחדיר את סקריפט התוכן אל https://www.nytimes.com/arts/index.html
ואל https://www.nytimes.com/jobs/index.htm*
, אבל לא אל https://www.nytimes.com/sports/index.html
:
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"include_globs": ["*nytimes.com/???s/*"],
"js": ["contentScript.js"]
}
],
...
}
התוסף הזה מחדיר את סקריפט התוכן אל https://history.nytimes.com
ואל https://.nytimes.com/history
, אבל לא אל https://science.nytimes.com
או אל https://www.nytimes.com/science
:
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"exclude_globs": ["*science*"],
"js": ["contentScript.js"]
}
],
...
}
אפשר לכלול את כל ההגדרות האלה, חלק מהן או רק אחת מהן כדי להשיג את ההיקף הנכון.
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"exclude_matches": ["*://*/*business*"],
"include_globs": ["*nytimes.com/???s/*"],
"exclude_globs": ["*science*"],
"js": ["contentScript.js"]
}
],
...
}
זמן ריצה
השדה run_at
קובע מתי קובצי JavaScript מוזרקים לדף האינטרנט. ערך ברירת המחדל המועדף הוא "document_idle"
. אפשר לעיין בסוג RunAt כדי לראות ערכים אפשריים אחרים.
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"run_at": "document_idle",
"js": ["contentScript.js"]
}
],
...
}
service-worker.js
chrome.scripting.registerContentScripts([{
id : "test",
matches : [ "https://*.nytimes.com/*" ],
runAt : "document_idle",
js : [ "contentScript.js" ],
}]);
שם | סוג | תיאור |
---|---|---|
document_idle |
מחרוזת | מומלץ. מומלץ להשתמש ב-"document_idle" בכל הזדמנות.הדפדפן בוחר מתי להוסיף סקריפטים בין "document_end" לבין הזמן שמיד אחרי
הפעלת האירוע window.onload . הרגע המדויק של ההזרקה תלוי במורכבות של המסמך ובמשך הזמן שנדרש לטעינה שלו, והוא מותאם למהירות הטעינה של הדף.סקריפטים של תוכן שפועלים ב- "document_idle" לא צריכים להאזין לאירוע window.onload , מובטח שהם יפעלו אחרי שה-DOM יושלם. אם סקריפט צריך לפעול אחרי window.onload , התוסף יכול לבדוק אם onload כבר הופעל באמצעות המאפיין document.readyState . |
document_start |
מחרוזת | הסקריפטים מוזרקים אחרי כל הקבצים מ-css , אבל לפני שנוצר DOM אחר או לפני שמופעל סקריפט אחר. |
document_end |
מחרוזת | סקריפטים מוזרקים מיד אחרי שה-DOM הושלם, אבל לפני שמשאבי משנה כמו תמונות ופריימים נטענו. |
ציון פריימים
במקרה של סקריפטים הצהרתיים של תוכן שצוינו במניפסט, השדה "all_frames"
מאפשר לתוסף לציין אם קובצי JavaScript ו-CSS צריכים להיות מוזרקים לכל המסגרות שתואמות לדרישות כתובת ה-URL שצוינו, או רק למסגרת העליונה בכרטיסייה:
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"all_frames": true,
"js": ["contentScript.js"]
}
],
...
}
כשרושמים סקריפטים של תוכן באופן פרוגרמטי באמצעות chrome.scripting.registerContentScripts(...)
, אפשר להשתמש בפרמטר allFrames
כדי לציין אם להחדיר את סקריפט התוכן לכל המסגרות שתואמות לדרישות כתובת ה-URL שצוינו, או רק למסגרת העליונה בכרטיסייה. אפשר להשתמש בפרמטר הזה רק עם tabId, ואי אפשר להשתמש בו אם מציינים frameIds או documentIds:
service-worker.js
chrome.scripting.registerContentScripts([{
id: "test",
matches : [ "https://*.nytimes.com/*" ],
allFrames : true,
js : [ "contentScript.js" ],
}]);
הוספה למסגרות קשורות
יכול להיות שתוספים ירצו להריץ סקריפטים בפריים שקשורים לפריים תואם, אבל הם עצמם לא תואמים. תרחיש נפוץ שבו זה קורה הוא כשמדובר במסגרות עם כתובות URL שנוצרו על ידי מסגרת תואמת, אבל כתובות ה-URL שלהן לא תואמות לדפוסים שצוינו בסקריפט.
זה קורה כשתוסף רוצה להחדיר לתוך מסגרות עם כתובות URL שיש להן סכמות של about:
, data:
, blob:
ו-filesystem:
. במקרים האלה, כתובת ה-URL לא תתאים לתבנית של סקריפט התוכן (ובמקרה של about:
ו-data:
, היא אפילו לא תכלול את כתובת ה-URL או המקור של ההורה, כמו ב-about:blank
או ב-data:text/html,<html>Hello, World!</html>
). עם זאת, עדיין אפשר לשייך את המסגרות האלה למסגרת שיוצרת אותן.
כדי להחדיר לתוך המסגרות האלה, התוספים יכולים לציין את המאפיין "match_origin_as_fallback"
במפרט של סקריפט תוכן במניפסט.
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.google.com/*"],
"match_origin_as_fallback": true,
"js": ["contentScript.js"]
}
],
...
}
אם מציינים את הערך true
, Chrome יבדוק את המקור של יוזם המסגרת כדי לקבוע אם המסגרת תואמת, ולא את כתובת ה-URL של המסגרת עצמה. שימו לב, יכול להיות שהערך הזה יהיה שונה מהמקור של מסגרת היעד (למשל, כתובות URL data:
מכילות מקור null).
המסגרת שיזמה את הפעולה היא המסגרת שיצרה את מסגרת היעד או ניווטה אליה. בדרך כלל מדובר בהורה או בחלון הפתיחה הישיר, אבל לא תמיד (למשל במקרה של מסגרת שמנווטת iframe בתוך iframe).
ההשוואה מתבצעת לפי המקור של פריים היוזם, ולכן פריים היוזם יכול להיות בכל נתיב מהמקור הזה. כדי להבהיר את המשמעות הזו, Chrome דורש שכל סקריפט תוכן שצוין עם "match_origin_as_fallback"
שמוגדר כ-true
יציין גם נתיב של *
.
אם מציינים גם "match_origin_as_fallback"
וגם "match_about_blank"
, המערכת נותנת עדיפות ל-"match_origin_as_fallback"
.
תקשורת עם הדף שבו מוטמעת המפה
למרות שסביבות ההפעלה של סקריפטים של תוכן והדפים שמארחים אותם מבודדות זו מזו, יש להן גישה משותפת ל-DOM של הדף. אם הדף רוצה לתקשר עם סקריפט התוכן, או עם התוסף דרך סקריפט התוכן, הוא צריך לעשות זאת דרך ה-DOM המשותף.
דוגמה לשימוש ב-window.postMessage()
:
content-script.js
var port = chrome.runtime.connect();
window.addEventListener("message", (event) => {
// We only accept messages from ourselves
if (event.source !== window) {
return;
}
if (event.data.type && (event.data.type === "FROM_PAGE")) {
console.log("Content script received: " + event.data.text);
port.postMessage(event.data.text);
}
}, false);
example.js
document.getElementById("theButton").addEventListener("click", () => {
window.postMessage(
{type : "FROM_PAGE", text : "Hello from the webpage!"}, "*");
}, false);
הדף שאינו תוסף, example.html, שולח הודעות לעצמו. ההודעה הזו נחסמת ונבדקת על ידי סקריפט התוכן, ואז היא נשלחת לתהליך של התוסף. כך הדף יוצר קו תקשורת לתהליך ההרחבה. אפשר לבצע את הפעולה ההפוכה באמצעות אמצעים דומים.
גישה לקובצי הרחבות
כדי לגשת לקובץ של תוסף מסקריפט תוכן, אפשר לקרוא ל-chrome.runtime.getURL()
כדי לקבל את כתובת ה-URL המלאה של נכס התוסף, כמו שמוצג בדוגמה הבאה (content.js
):
content-script.js
let image = chrome.runtime.getURL("images/my_image.png")
כדי להשתמש בגופנים או בתמונות בקובץ CSS, אפשר להשתמש ב-@@extension_id
כדי ליצור כתובת URL כמו בדוגמה הבאה (content.css
):
content.css
body {
background-image:url('chrome-extension://__MSG_@@extension_id__/background.png');
}
@font-face {
font-family: 'Stint Ultra Expanded';
font-style: normal;
font-weight: 400;
src: url('chrome-extension://__MSG_@@extension_id__/fonts/Stint Ultra Expanded.woff') format('woff');
}
צריך להצהיר על כל הנכסים כמשאבים שנגישים באינטרנט בקובץ manifest.json
:
manifest.json
{
...
"web_accessible_resources": [
{
"resources": [ "images/*.png" ],
"matches": [ "https://example.com/*" ]
},
{
"resources": [ "fonts/*.woff" ],
"matches": [ "https://example.com/*" ]
}
],
...
}
Content Security Policy
לסקריפטים של תוכן שפועלים בעולמות מבודדים יש את מדיניות אבטחת התוכן (CSP) הבאה:
script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' chrome-extension://abcdefghijklmopqrstuvwxyz/; object-src 'self';
בדומה להגבלות שחלות על הקשרים אחרים של התוסף, ההגבלה הזו מונעת את השימוש ב-eval()
וגם את טעינת סקריפטים חיצוניים.
בתוספים לא ארוזים, ה-CSP כולל גם את localhost:
script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' http://localhost:* http://127.0.0.1:* chrome-extension://abcdefghijklmopqrstuvwxyz/; object-src 'self';
כשמזריקים סקריפט תוכן לעולם הראשי, מדיניות ה-CSP של הדף חלה.
אבטחת החשבון
למרות שהעולמות המבודדים מספקים שכבת הגנה, שימוש בסקריפטים של תוכן עלול ליצור נקודות חולשה בתוסף ובדף האינטרנט. אם סקריפט התוכן מקבל תוכן מאתר נפרד, למשל על ידי קריאה ל-fetch()
, חשוב לסנן את התוכן מפני מתקפות סקריפטינג חוצה אתרים לפני שמזריקים אותו. התקשורת צריכה להתבצע רק באמצעות HTTPS כדי למנוע התקפות מסוג "man-in-the-middle".
חשוב להקפיד לסנן דפי אינטרנט זדוניים. לדוגמה, הדפוסים הבאים מסוכנים ואסורים ב-Manifest V3:
content-script.js
const data = document.getElementById("json-data"); // WARNING! Might be evaluating an evil script! const parsed = eval("(" + data + ")");
content-script.js
const elmt_id = ... // WARNING! elmt_id might be '); ... evil script ... //'! window.setTimeout("animate(" + elmt_id + ")", 200);
במקום זאת, מומלץ להשתמש בממשקי API בטוחים יותר שלא מריצים סקריפטים:
content-script.js
const data = document.getElementById("json-data") // JSON.parse does not evaluate the attacker's scripts. const parsed = JSON.parse(data);
content-script.js
const elmt_id = ... // The closure form of setTimeout does not evaluate scripts. window.setTimeout(() => animate(elmt_id), 200);