- PVSM.RU - https://www.pvsm.ru -

Переводим Chrome extension на manifest_version 2

Владельцам расширений (а также приложений) для Хрома уже пора бы задуматься над поддержкой второй версии манифеста.
Если кто не в курсе, то не так давно были объявлены новые изменения и нововведения в разработку расширений для браузера.
Далее будет выборочный перевод двух [1] страниц [2] и мой способ использования шаблонизатора изнутри песочницы.

Сначала немного о планах Гугла [3] по поддержке старых расширений

* Далее старыми расширениями буду называть расширения и приложения с версией манифеста 1 (или вообще без версии).

  • Начиная с Chrome 21 блокируется создание новых расширений с первой версией манифеста, но разрешаются обновления существующих расширений до старой версии манифеста.
  • С выходом Chrome 23 (начало ноября) у Веб-сторе [4] будет заблокировано обновление на расширения со старыми версиями манифеста. Хром перестанет запаковывать старые расширения и загружать распакованные для разработки.
  • Первая четверть 2013 — старые расширения уже будет не найти в Веб-сторе. Разработчикам об этом сообщат по email.
  • Вторая четверть 2013 — из Веб-стора будут удалены все старые расширения, а разработчикам придет еще одно уведомление. Но Хром пока будет загружать и запускать установленные расширения с манифестом версии 1.
  • Третья четверть 2013 — Хром перестанет загружать и запускать старые расширения.

Различия версий манифеста 1 и 2

  • Политика безопасности контента (content security policy) [2] по умолчанию установлена в `script-src 'self'; object-src 'self'. Это по сути самое важное обновление. О нем немного позже.
  • Все ресурсы расширения теперь недоступны по URL chrome-extension://[PACKAGE ID]/[PATH]. Т.е. вы не сможете подключить скрипт или картинку с расширения с других страниц кроме самого расширения. Но чтобы обойти этот недостаток появилось свойство web_accessible_resources [5], в котором можно указать массив с путями к нужным ресурсам.
  • Вместо свойства background_page (которое было строкой), теперь пишем background [6], которое должно содержать объект со свойством scripts или page.
  • Изменения в browser actions:
    • поле browser_actions заменено на browser_action, а API chrome.browserActions — на chrome.browserAction.
    • удалено свойство icons из browser_action. Вместо него нужно использовать default_icon или chrome.browserAction.setIcon.
    • удалено свойство name из browser_action. Вместо него нужно использовать default_title или chrome.browserAction.setTitle.
    • удалено свойство popup из browser_action. Вместо него нужно использовать default_popup или chrome.browserAction.setPopup.
    • свойство default_popup в browser_action должно быть строкой, а не объектом

  • Изменения в page actions:
    • поле page_actions заменено на page_action, а API chrome.pageActions — на chrome.pageAction.
    • удалено свойство icons из page_action. Вместо него нужно использовать default_icon или chrome.pageAction.setIcon.
    • удалено свойство name из page_action. Вместо него нужно использовать default_title или chrome.pageAction.setTitle.
    • удалено свойство popup из page_action. Вместо него нужно использовать default_popup или chrome.pageAction.setPopup.
    • свойство default_popup в page_action должно быть строкой, а не объектом.
    • Удалено chrome.self из API, теперь нужно использовать chrome.extension.

  • Больше нет chrome.extension.getTabContentses и chrome.extension.getExtensionTabs. Вместо них нужно использовать chrome.extension.getViews({ "type": "tab" }) [7].
  • Вместо Port.tab используем Port.sender.

Политика безопасности контента (Content Security Policy [2] или CSP)

Чтобы расширения были менее подвержены XSS уязвимостям были внедрены общие принципы CSP. В общем CSP являет собой механизм белых и черных списков касательно ресурсов, которые загружаются и выполняются расширением. С помощью CSP можно установить только необходимые разрешения для расширения и таким образом повысить его безопасность.
Эта политика является дополнительным уровнем защиты над правами на доступ к ресурсам (host permissions [8])

Установить политику безопасности можно в строковом параметре content_security_policy в manifest.json.
Если не установлена версия манифеста (manifest_version), то по умолчанию нет никакой политики безопасности контента. Но для второй версии манифеста она установлена по умолчанию со значением script-src 'self'; object-src 'self'. Поэтому есть некоторые ограничения. Например, функция eval выполнятся не будет. Так же не будут выполнятся инлайновые <script> блоки и инлайновые обработчики событий (<button onclick="...">). Если вы в коде по какой-то причине передавали строку в качестве первого аргумента функций setTimeout и setInterval, то это тоже придется исправить.
Так же скрипты, подключаемые с других ресурсов (с CDN, например), нужно сохранить локально.

Смягчение ограничений политики безопасности [9]

К сожалению нет способа смягчить ограничения на выполнение инлайновых скриптов. Но есть возможность подключать сторонние скрипты (правда, только по https). Для этого нужно указать домен в content_security_policy, например так:

{
  ...,
  "content_security_policy": "script-src 'self' https://example.com; object-src 'self'",
  ...
}

“Что же делать если нужно выполнять eval?” или “Как подключить шаблонизатор?”

Сначала вы можете подумать — зачем мне eval? Но эта функция нужна почти для всех шаблонизаторов (заметьте, new Function() тоже не работает). Здесь нам поможет песочница (sandbox [10]).
Можно создать отдельную страницу, где будут выполнятся все небезопасные операции (например, eval) и запускать ее изнутри песочницы (на песочницу по умолчанию CSP не распространяется).
Так же есть возможность передавать данные между расширением и песочницей через метод postMessage().

Далее расскажу как я подключал underscore шаблонизатор [11]

  1. Создадим файл sandboxed/template-renderer.html с таким вот контентом
    template-renderer.html

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <title>Sandboxed Template Renderer</title>
        <script src="/js/libs/underscore/underscore-min.js"></script>
    </head>
    <body>
        <script>
            var templates = {};
            window.addEventListener('message', function (event) {
                var template;
                if (typeof templates[event.data.templateName] == 'undefined') {
                    template = _.template(event.data.template);
                    templates[event.data.templateName] = template;
                } else {
                    template = templates[event.data.templateName];
                }
                event.source.postMessage({
                    id: event.data.id,
                    result: template(event.data.context)
                }, event.origin);
            });
        </script>
    </html>
    

  2. В манифесте добавим этот html в песочницу
    {
    ...,
        "sandbox": {
            "pages": ["sandboxed/template-renderer.html"]
        },
    ...
    }
    

  3. Создадим функцию которая будет обращаться к нашей песочнице за рендерингом шаблона
    function getTemplate

    var getTemplate = (function(){
    
        var iframe = document.createElement('iframe'),
            callbacks = [];
    
        iframe.src = 'sandboxed/template-renderer.html';
        iframe.style.display = 'none';
        document.body.appendChild(iframe);
    
        window.addEventListener('message', function (event) {
            callbacks.forEach(function (item, idx) {
                if (item && item.id == event.data.id) {
                    item.callback(event.data.result);
                    delete callbacks[idx];
                }
            });
        });
    
        return function (templateName, template) {
            return function (context, callback) {
                var id = Math.random();
                callbacks.push({
                    id: id,
                    callback: callback
                });
                iframe.contentWindow.postMessage({
                    id: id,
                    templateName: templateName,
                    template: template,
                    context: context
                }, '*');
            };
        };
    }());
    

  4. Шаблонизатор готов к использованию
    // получаем функцию, которая будет рендерить шаблон в зависимости от контекста
    var template = getTemplate('templateId', templateContent);
    // одно плохо - теперь результат получаем асинхронно
    template({text: 'Hello world'}, function (html) {
        // выводим html на страницу
        // можно было передать $('body').html в качестве второго параметра,
        // но решил написать так для большей наглядности
        $('body').html(html);
    });
    

Это решение — первое что пришло в голову. Наверное есть способ сделать это лучше (красивее). Буду рад увидеть предложения в комментариях.

И на последок, мой manifest.json:

manifest.json

{
    "name": "Twittext",
    "description": "A lightweight Google Chrome extension for Twitter",
    "background": {
        "page": "background.html"
    },
    "manifest_version": 2,
    "browser_action": {
        "default_icon": "img/icon_19.png",
        "default_title": "Twittext",
        "default_popup": "popup.html"
    },
    "icons": {
        "128": "img/icon_128.png",
        "19": "img/icon_19.png",
        "48": "img/icon_48.png"
    },
    "options_page": "options.html",
    "version": "1.6.1",
    "permissions": [
        "tabs",
        "background",
        "https://api.twitter.com/",
        "https://userstream.twitter.com/"
    ],
    "sandbox": {
        "pages": ["sandboxed/template-renderer.html"]
    }
}

Автор: utf


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/chrome/13494

Ссылки в тексте:

[1] двух: http://developer.chrome.com/extensions/manifestVersion.html

[2] страниц: http://developer.chrome.com/extensions/contentSecurityPolicy.html

[3] планах Гугла: http://developer.chrome.com/extensions/manifestVersion.html#schedule

[4] Веб-сторе: https://chrome.google.com/webstore

[5] web_accessible_resources: http://developer.chrome.com/extensions/manifest.html#web_accessible_resources

[6] background: http://developer.chrome.com/extensions/background_pages.html

[7] chrome.extension.getViews({ "type": "tab" }): http://developer.chrome.com/extensions/extension.html#method-getViews

[8] host permissions: http://developer.chrome.com/extensions/manifest.html#permissions

[9] Смягчение ограничений политики безопасности: http://developer.chrome.com/extensions/contentSecurityPolicy.html#H2-3

[10] sandbox: http://developer.chrome.com/extensions/manifest.html#sandbox

[11] underscore шаблонизатор: http://underscorejs.org/#template