Тварь ли я дрожащая или право имею? Берем чужие сайты под свой контроль. Часть 2 — Пользовательские скрипты в Chrome

от автора

Сегодня продолжаем тему написания расширения для Chrome, позволяющего внедрять свой код на чужие сайты, тем самым меняя или дополняя их функционал и внешний вид по своему желанию. Для чего это нужно и чем может быть полезно, рассмотрено в предыдущей части. Также в предыдущей статье были рассмотрены вопросы, касающиеся настройки и подготовки к написанию расширения с использованием Angular 18. Напоминаю, что весь код я публикую в открытом доступе на GitHub.

Пользовательские скрипты в Chrome

Давайте разберемся, как устроен функционал внедрения своего произвольного кода на сторонние сайты в Chrome. В этом году состоялся переход на новую платформу для расширений Manifest V3. Ранее, во 2-й версии Manifest, внедрение произвольного кода на страницу не выделялось в API, в результате чего, хотя и были некоторые ограничения со стороны платформы, процесс внедрения имел различные варианты решения со своими нюансами, разработчик сам выбирал и реализовывал подходящий. Однако в текущей версии этот процесс стал более унифицирован, для регистрации и других операций со скриптами используется задокументированное API userScripts.

Для получения возможности работать с пользовательскими скриптами в нашем расширении нам понадобится запросить разрешение на данные операции. В manifest.json в раздел permissions нужно добавить userScripts, а также в разделе host_permissions задать список сайтов, на которые мы будем внедрять свой код. Если нам необходима возможность внедряться на абсолютно любой сайт, то можно задать такую маску: "host_permissions": [ "http://*/*", "https://*/*" ]. Для работы с API Chrome в Angular также потребуется установить описание структуры API: npm install @types/chrome --save-dev. После этого нам становится доступно API Chrome, и прежде всего нас интересует отдельная его часть userScripts.

Упрощенная структура chrome.userScripts API (отображены только используемые нами элементы, полная документация доступна на официальном сайте)

Упрощенная структура chrome.userScripts API (отображены только используемые нами элементы, полная документация доступна на официальном сайте)

Регистрация пользовательского скрипта

Чтобы внедрить свой JavaScript код на произвольную страницу какого-либо сайта, необходимо зарегистрировать скрипт с помощью chrome.userScripts.register:

chrome.userScripts.register([{   id: 'habr-test',   matches: ['https://habr.com/*'],   js: [{code: 'alert("Wow! I\'ve hacked HABR!")'}] }]);

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

  • id — уникальный символьный код регистрируемого скрипта

  • matches — массив строк со списком URL-адресов, на которые внедряется наш скрипт. Вместо адресов можно использовать шаблоны.

  • excludeMatches — аналогично параметру matches, только работает как исключающий фильтр URL-адресов

  • js — массив JavaScript-кодов, представленных в виде объектов типа chrome.userScripts.ScriptSource. Почему массив? Потому что вы можете в одном регистрируемом пользовательском скрипте подключить сразу несколько отдельных кодов, разделенных по смыслу или своему функциональному назначению.

  • runAt — определяет, в какой момент запускать пользовательский скрипт: document_start, document_end или document_idle

Получение списка зарегистрированных скриптов

Для получения списка ранее зарегистрированных скриптов используем функцию chrome.userScripts.getScripts, которая возвращает Promise с результатом. В качестве параметра в функцию можно передать фильтр, например, для получения скрипта с заданным id:

chrome.userScripts.getScripts({ids: ['habr-test']})

Обновление зарегистрированных скриптов

Формат функции chrome.userScripts.update абсолютно идентичен функции для регистрации, только в качестве параметра нужно передавать не список новых скриптов, а список обновляемых. Если в каком-либо из переданных скриптов будет задан несуществующий id, выполнение кода будет прервано с ошибкой.

chrome.userScripts.update([{   id: 'habr-test',   matches: ['https://habr.com/*'],   js: [{code: 'alert("Wow! I\'ve hacked HABR twice!")'}] }]);

Удаление пользовательского скрипта

Вызов функции удаления аналогичен вызову функции получения списка скриптов — в качестве параметра передается фильтр, в котором мы можем задать список id удаляемых скриптов:

chrome.userScripts.getScripts({ids: ['habr-test']})

Скрипты, скрипты, а где CSS?

Ранее, в предыдущей статье, я озвучивал, что готовое расширение сможет подключать связку JS + CSS кода для любых страниц. Однако пока о CSS я не написал ни слова. Почему? Потому что в Chrome реализовано только подключение JS-скриптов. Но разве это проблема? Конечно нет, мы можем с помощью JS реализовать подключение любого CSS-кода самостоятельно точно также, как это делалось со старой версией Manifest V2.

private wrapCSSCodeWithJS(codeCSS: string): string {     return '(() => {' +       '\n\tconst style = document.createElement(\'style\')' +       '\n\tstyle.textContent = /*user style start*/`' + codeCSS + '`/*user style end*/' +       '\n\tdocument.documentElement.appendChild(style)' +       '\n})()'   }

В результате выполнения функции мы получаем JS-код для подключения заданного нами CSS-кода, т. е. мы его как бы оборачиваем в JS-код. А далее уже он может быть зарегистрирован как обычный пользовательский скрипт.

Developer mode

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

Есть только один неприятный нюанс. Зачем-то в команде разработчиков Chrome решили, что недостаточно запроса вида Read and change all your data on all websites, с которым пользователь соглашается при установке расширения. Чтобы разрешить использование userScripts API, пользователь должен помимо этого обязательно включить еще и галочку Developer mode (Режим разработчика) на странице списка установленных расширений.

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

Наверное, это не такая большая проблема даже для тех, кто с компьютером на Вы. В конце концов, если пользователь ищет и ставит какие-то дополнительные расширения для браузера, для него не должно быть проблемой найти в настройках нужную опцию. Однако, установив расширение, пользователь обычно ожидает, что им можно сразу пользоваться без дополнительных манипуляций. К тому же разработчик расширения должен сам позаботиться о том, чтобы уведомить пользователя о необходимости дополнительной настройки. Для этого необходимо в коде проверять, включен ли «Режим разработчика» (и далее реализовать уведомление пользователя о необходимости включить дополнительную опцию для работы расширения):

isChromeUserscriptsAvailable(): boolean {     try {       chrome.userScripts;       return true;     } catch {       return false;     }   }

Развитие функционала

Если посмотреть на представленный Chrome’ом userScripts API, выглядит он очень скудно, с дополнительным функционалом тут не разгуляешься. Например, возьмем простейшую задачу — временное включение/отключение пользовательских скриптов.

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

Сохранять будем в chrome.storage.local — специальное локальное хранилище, доступное расширениям. Для его использования в manifest.json в раздел permisions необходимо добавить storage и unlimitedStorage (без второго разрешения максимальный размер хранимых данных будет ограничен 10 Мб). Создаваемая структура будет связана по id с объектом RegisteredUserScript, сохраненным в браузере.

Также здесь мы дублируем и остальные свойства сохраненного пользовательского скрипта. Зачем дублировать? Во-первых, так удобнее будет работать, когда все данные будут извлекаться из одного места. Во-вторых, и основное — для реализации требуемого нам функционала (включения/отключения скрипта) нам потребуется удалять зарегистрированный в браузере объект (т. к. он не имеет состояния «отключен»). При этом сами данные скрипта — код, привязка к URL — должны сохраниться. Они и будут храниться в создаваемой нами дополнительной структуре:

export interface CodeBundle {     id: string     urlPatterns: string[]     urlPatternsCommaSeparated?: string     js: string     css: string     isEnabled: boolean   }

Здесь мы добавили дополнительное свойство isEnabled — именно на основании него мы теперь можем включать/отключать скрипты, т. е. по сути регистрировать и отменять их регистрацию в браузере.

В итоге, реализовав все вышеописанное в коде, мы получаем интерфейс вот такого вида, где мы можем управлять созданными нами скриптами и стилями, привязанными к заданным URL:

Быстрый доступ к функционалу

Чтобы постоянно не лезть в настройки расширения и упростить процесс добавления/редактирования кода, следует добавить возможность добавлять/редактировать код для текущей открытой страницы или сайта в пару кликов. Для этого мы используем специальное всплывающее окно, которое появляется при нажатии на иконку нашего расширения (как включить данную возможность, рассматривалось в предыдущей статье). При открытии данного всплывающего окна нам необходимо получить URL текущей активной вкладки. Для этого добавим простую функцию, использующую Tabs API:

getCurrentUrlAsync(): Promise<string | undefined> {     return chrome.tabs.query({       active: true,       currentWindow: true,     }).then((tabs: chrome.tabs.Tab[]) => tabs[0].url)   }

Чтобы работать с данным API, нам необходимо также добавить в manifest.json дополнительный запрос на разрешение tabs в раздел permissions.

Теперь, получив текущий URL, мы можем перебрать все добавленные нами пользовательские скрипты, сравнить шаблоны URL (в т. ч. исключающие) с текущим URL и отобразить во всплывающем окне те скрипты, которые привязаны к текущей странице. Также здесь реализуем возможность быстрого добавления скрипта к текущей странице или всему сайту. В итоге, кликнув по иконке установленного расширения, получаем такое всплывающее окно:

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


ссылка на оригинал статьи https://habr.com/ru/articles/858078/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *