网站可以调用浏览器扩展吗? [英] Can a site invoke a browser extension?

查看:21
本文介绍了网站可以调用浏览器扩展吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我是浏览器扩展开发的新手,我了解浏览器扩展改变页面并向其中注入代码的概念.

有没有办法可以扭转这个方向?我编写了一个提供一组 API 的扩展,想要使用我的扩展的网站可以检测它的存在,如果它存在,网站可以调用我的 API 方法,如 var extension = Extenion(foo, bar).这在 Chrome、Firefox 和 Safari 中是否可行?

示例:

  1. Google 创建了一个名为 BeautifierExtension 的新扩展程序.它有一组 API 作为 JS 对象.

  2. 用户访问 reddit.com.Reddit.com 检测 BeautifierExtension 并通过调用 beautifer = Beautifier();

  3. 调用 API

参见#2 - 通常它是检测匹配站点并更改页面的扩展程序.我有兴趣知道#2 是否可行.

解决方案

自从 Chrome 引入 externally_connectable,这在 Chrome 中很容易做到.首先,在您的 manifest.json 文件中指定允许的域:

externally_connectable":{匹配":[*://*.example.com/*"]}

使用 chrome.runtime.sendMessage从页面发送消息:

chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},功能(响应){//...});

最后,使用 chrome.runtime.onMessageExternal:

chrome.runtime.onMessageExternal.addListener(功能(请求,发件人,sendResponse){//验证 `sender.url`,读取 `request` 对象,回复 `sednResponse(...)`...});

<小时>

如果您无权获得 externally_connectable 支持,原始答案如下:

我将从以 Chrome 为中心的角度来回答,尽管这里描述的原则(网页脚本注入、长时间运行的后台脚本、消息传递)几乎适用于所有浏览器扩展框架.

从高层次来看,您想要做的是将内容脚本注入每个网页,都添加了一个 API,可供网页访问.当站点调用 API 时,API 会触发内容脚本执行某些操作,例如通过异步回调向后台页面发送消息和/或将结果发送回内容脚本.

这里的主要困难是注入"到网页中的内容脚本不能直接改变 JavaScript 页面的执行环境.它们共享 DOM,因此 事件DOM 结构的更改 在内容脚本和网页之间共享,但函数和变量不共享.示例:

  • DOM 操作:如果内容脚本将 <div> 元素添加到页面,这将按预期工作.内容脚本和页面都会看到新的

    .

  • 事件:如果一个内容脚本设置了一个事件监听器,例如,点击一个元素,事件发生时监听器将成功触发.如果页面为从内容脚本触发的自定义事件设置了侦听器,则当内容脚本触发这些事件时,它们将被成功接收.

  • 函数:如果内容脚本定义了一个新的全局函数 foo()(就像您在设置新 API 时可能尝试的那样).页面不能看到或执行foo,因为foo只存在于内容脚本的执行环境中,而不存在于页面的环境中.

那么,如何设置合适的 API?答案有很多步骤:

  1. 在低级别,使您的 API 事件-基于.网页使用 dispatchEvent 触发自定义 DOM 事件,内容脚本使用 addEventListener 侦听它们,并在收到它们时采取行动.这是一个简单的基于事件的存储 API,网页可以使用它来让扩展程序为其存储数据:

    content_script.js(在您的扩展程序中):

    //一个对象,用于存储从 API 传入的东西内部存储 = {};//侦听从带有键/值对数据的页面触发的 myStoreEventdocument.addEventListener('myStoreEvent', function(event) {var dataFromPage = event.detail;internalStorage[dataFromPage.key] = dataFromPage.value});

    非扩展网页,使用基于事件的 API:

    function sendDataToExtension(key, value) {var dataObj = {"key":key, "value":value};var storeEvent = new CustomEvent('myStoreEvent', {"detail":dataObj});document.dispatchEvent(storeEvent);}sendDataToExtension("你好", "世界");

    如您所见,普通网页正在触发内容脚本可以看到并做出反应的事件,因为它们共享 DOM.事件附加了数据,添加到 CustomEvent构造函数.我这里的示例非常简单——一旦获得页面数据(很可能 传递背景页面进行进一步处理).

  2. 然而,这只是成功的一半.在我上面的例子中,普通网页必须自己创建 sendDataToExtension.创建和触发自定义事件非常冗长(我的代码占用 3 行并且相对简短).您不想仅仅为了使用您的 API 就强迫站点编写神秘的事件触发代码.解决方案有点麻烦:将 <script> 标记附加到您的共享 DOM,这会将事件触发代码添加到主页的执行环境中.

    content_script.js 内部:

    //从扩展文件中注入一个脚本//进入主页面的执行环境var s = document.createElement('script');s.src = chrome.extension.getURL("myapi.js");document.documentElement.appendChild(s);

    myapi.js 中定义的任何函数都可以访问主页.(如果您使用 "manifest_version":2,则需要在清单的 web_accessible_resources).

    myapi.js:

    function sendDataToExtension(key, value) {var dataObj = {"key":key, "value":value};var storeEvent = new CustomEvent('myStoreEvent', {"detail":dataObj});document.dispatchEvent(storeEvent);}

    现在普通网页可以简单地做到:

    sendDataToExtension("hello", "world");

  3. 我们的 API 流程还有另外一个问题:myapi.js 脚本在加载时将不可用.相反,它将在页面加载时间后加载一段时间.因此,纯网页需要知道何时可以安全地调用您的 API.您可以通过让 myapi.js 触发API 就绪"事件来解决此问题,您的页面会监听该事件.

    myapi.js:

    function sendDataToExtension(key, value) {//如上}//因为这个脚本正在运行,myapi.js 已经加载,所以让页面知道var customAPILoaded = new CustomEvent('customAPILoaded');document.dispatchEvent(customAPILoaded);

    普通网页使用 API:

    document.addEventListener('customAPILoaded', function() {sendDataToExtension("你好", "世界");//所有 API 交互都在这里进行,现在 API 已加载...});

  4. 加载时脚本可用性问题的另一个解决方案是将清单中的内容脚本的 run_at 属性设置为 "document_start",如下所示:

    ma​​nifest.json:

     "content_scripts": [{"匹配": ["https://example.com/*"],js":[myapi.js"],"run_at": "document_start"}],

    摘自文档:

    <块引用>

    在document_start"的情况下,文件在来自 css 的任何文件之后注入,但在构建任何其他 DOM 或运行任何其他脚本之前.

    对于一些可能比API 加载"事件更合适、更省力的内容脚本.

  5. 为了将结果返回发送到页面,您需要提供一个异步回调函数.无法从您的 API 同步返回结果,因为事件触发/侦听本质上是异步的(即,您的站点端 API 函数在内容脚本通过 API 请求获取事件之前终止).

    myapi.js:

    function getDataFromExtension(key, callback) {var reqId = Math.random().toString();//此请求的唯一 IDvar dataObj = {"key":key, "reqId":reqId};var fetchEvent = new CustomEvent('myFetchEvent', {"detail":dataObj});document.dispatchEvent(fetchEvent);//准备好接受内容脚本的回复document.addEventListener('fetchResponse', function respListener(event) {var data = event.detail;//检查此响应是否针对此请求if(data.reqId == reqId) {回调(数据.值);document.removeEventListener('fetchResponse', respListener);}}}

    content_script.js(在您的扩展程序中):

    //侦听从带有键的页面触发的 myFetchEvent//然后用回复触发 fetchResponse 事件document.addEventListener('myStoreEvent', function(event) {var dataFromPage = event.detail;var responseData = {"value":internalStorage[dataFromPage.key], "reqId":data.reqId};var fetchResponse = new CustomEvent('fetchResponse', {"detail":responseData});document.dispatchEvent(fetchResponse);});

    普通网页:

    document.addEventListener('customAPILoaded', function() {getDataFromExtension("hello", function(val) {alert("扩展名是" + val);});});

    reqId 是必要的,以防您同时发出多个请求,以免他们读取错误的响应.

我认为这就是一切!因此,当您考虑到其他扩展也可以将侦听器绑定到您的事件以窃听页面如何使用您的 API 时,不适合胆小的人,而且可能不值得.我之所以知道这一切,是因为我为学校项目制作了一个概念验证密码学 API(随后了解了与之相关的主要安全陷阱).

总而言之:内容脚本可以监听来自普通网页的自定义事件,该脚本还可以注入带有函数的脚本文件,使网页更容易触发这些事件.内容脚本可以将消息传递到后台页面,然后后台页面存储、转换或传输来自消息的数据.

I am a newbie to the browser extension development and I understand the concept of browser extensions altering the page and injecting codes into it.

Is there a way this direction can be turned around? I write an extension that provides a set of APIs, and web sites that want to use my extension can detect its presence and if it is present, the website can call my API methods like var extension = Extenion(foo, bar). Is this possible in Chrome, Firefox and Safari?

Example:

  1. Google created a new extension called BeautifierExtension. It has a set of APIs as JS objects.

  2. User goes to reddit.com. Reddit.com detects BeautifierExtension and invoke the API by calling beautifer = Beautifier();

See #2 - normally it's the extension that detects the matching sites and alter the pages. What I am interested to know is whether #2 is possible.

解决方案

Since Chrome introduced externally_connectable, this is quite easy to do in Chrome. First, specify the allowed domain in your manifest.json file:

"externally_connectable": {
  "matches": ["*://*.example.com/*"]
}

Use chrome.runtime.sendMessage to send a message from the page:

chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
  function(response) {
    // ...
  });

Finally, listen in your background page with chrome.runtime.onMessageExternal:

chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    // verify `sender.url`, read `request` object, reply with `sednResponse(...)`...
  });


If you don't have access to externally_connectable support, the original answer follows:

I'll answer from a Chrome-centric perspective, although the principles described here (webpage script injections, long-running background scripts, message passing) are applicable to virtually all browser extension frameworks.

From a high level, what you want to do is inject a content script into every web page, which adds an API, accessible to the web page. When the site calls the API, the API triggers the content script to do something, like sending messages to the background page and/or send a result back to the content script, via asynchronous callback.

The main difficulty here is that content scripts which are "injected" into a web page cannot directly alter the JavaScript execution environment of a page. They share the DOM, so events and changes to DOM structure are shared between the content script and the web page, but functions and variables are not shared. Examples:

  • DOM manipulation: If a content script adds a <div> element to a page, that will work as expected. Both content script and page will see the new <div>.

  • Events: If a content script sets up an event listener, e.g., for clicks on an element, the listener will successfully fire when the event occurs. If the page sets up a listener for custom events fired from the content script, they will be successfully received when the content script fires those events.

  • Functions: If the content script defines a new global function foo() (as you might try when setting up a new API). The page cannot see or execute foo, because foo exists only in the content script's execution environment, not in the page's environment.

So, how can you set up a proper API? The answer comes in many steps:

  1. At a low-level, make your API event-based. The web page fires custom DOM events with dispatchEvent, and the content scripts listens for them with addEventListener, taking action when they are received. Here's a simple event-based storage API which a web page can use to have the extension to store data for it:

    content_script.js (in your extension):

    // an object used to store things passed in from the API
    internalStorage = {};
    
    // listen for myStoreEvent fired from the page with key/value pair data
    document.addEventListener('myStoreEvent', function(event) {
        var dataFromPage = event.detail;
        internalStorage[dataFromPage.key] = dataFromPage.value
    });
    

    Non-extension web page, using your event-based API:

    function sendDataToExtension(key, value) {
        var dataObj = {"key":key, "value":value};
        var storeEvent = new CustomEvent('myStoreEvent', {"detail":dataObj});
        document.dispatchEvent(storeEvent);
    }
    sendDataToExtension("hello", "world");
    

    As you can see, the ordinary web page is firing events that the content script can see and react to, because they share the DOM. The events have data attached, added in the CustomEvent constructor. My example here is pitifully simple -- you can obviously do much more in your content script once it has the data from the page (most likely pass it to the background page for further processing).

  2. However, this is only half the battle. In my example above, the ordinary web page had to create sendDataToExtension itself. Creating and firing custom events is quite verbose (my code takes up 3 lines and is relatively brief). You don't want to force a site to write arcane event-firing code just to use your API. The solution is a bit of a nasty hack: append a <script> tag to your shared DOM which adds the event-firing code to the main page's execution environment.

    Inside content_script.js:

    // inject a script from the extension's files
    // into the execution environment of the main page
    var s = document.createElement('script');
    s.src = chrome.extension.getURL("myapi.js");
    document.documentElement.appendChild(s);
    

    Any functions that are defined in myapi.js will become accessible to the main page. (If you are using "manifest_version":2, you'll need to include myapi.js in your manifest's list of web_accessible_resources).

    myapi.js:

    function sendDataToExtension(key, value) {
        var dataObj = {"key":key, "value":value};
        var storeEvent = new CustomEvent('myStoreEvent', {"detail":dataObj});
        document.dispatchEvent(storeEvent);
    }
    

    Now the plain web page can simply do:

    sendDataToExtension("hello", "world");
    

  3. There is one further wrinkle to our API process: the myapi.js script will not be available exactly at load time. Instead, it will be loaded some time after page-load time. Therefore, the plain web page needs to know when it can safely call your API. You can solve this by having myapi.js fire an "API ready" event, which your page listens for.

    myapi.js:

    function sendDataToExtension(key, value) {
        // as above
    }
    
    // since this script is running, myapi.js has loaded, so let the page know
    var customAPILoaded = new CustomEvent('customAPILoaded');
    document.dispatchEvent(customAPILoaded);
    

    Plain web page using API:

    document.addEventListener('customAPILoaded', function() {
        sendDataToExtension("hello", "world");
        // all API interaction goes in here, now that the API is loaded...
    });
    

  4. Another solution to the problem of script availability at load time is setting run_at property of content script in manifest to "document_start" like this:

    manifest.json:

        "content_scripts": [
          {
            "matches": ["https://example.com/*"],
            "js": [
              "myapi.js"
            ],
            "run_at": "document_start"
          }
        ],
    

    Excerpt from docs:

    In the case of "document_start", the files are injected after any files from css, but before any other DOM is constructed or any other script is run.

    For some contentscripts that could be more appropriate and of less effort than having "API loaded" event.

  5. In order to send results back to the page, you need to provide an asynchronous callback function. There is no way to synchronously return a result from your API, because event firing/listening is inherently asynchronous (i.e., your site-side API function terminates before the content script ever gets the event with the API request).

    myapi.js:

    function getDataFromExtension(key, callback) {
        var reqId = Math.random().toString(); // unique ID for this request
        var dataObj = {"key":key, "reqId":reqId};
        var fetchEvent = new CustomEvent('myFetchEvent', {"detail":dataObj});
        document.dispatchEvent(fetchEvent);
    
        // get ready for a reply from the content script
        document.addEventListener('fetchResponse', function respListener(event) {
            var data = event.detail;
    
            // check if this response is for this request
            if(data.reqId == reqId) {
                callback(data.value);
                document.removeEventListener('fetchResponse', respListener);
            }
        }
    }
    

    content_script.js (in your extension):

    // listen for myFetchEvent fired from the page with key
    // then fire a fetchResponse event with the reply
    document.addEventListener('myStoreEvent', function(event) {
        var dataFromPage = event.detail;
        var responseData = {"value":internalStorage[dataFromPage.key], "reqId":data.reqId};
        var fetchResponse = new CustomEvent('fetchResponse', {"detail":responseData});
        document.dispatchEvent(fetchResponse);
    });
    

    ordinary web page:

    document.addEventListener('customAPILoaded', function() {
        getDataFromExtension("hello", function(val) {
            alert("extension says " + val);
        });
    });
    

    The reqId is necessary in case you have multiple requests out at once, so that they don't read the wrong responses.

And I think that's everything! So, not for the faint of heart, and possibly not worth it, when you consider that other extensions can also bind listeners to your events to eavesdrop on how a page is using your API. I only know all this because I made made a proof-of-concept cryptography API for a school project (and subsequently learned the major security pitfalls associated with it).

In sum: A content script can listen for custom events from an ordinary web page, and the script can also inject a script file with functions that makes it easier for web pages to fire those events. The content script can pass messages to a background page, which then stores, transforms, or transmits data from the message.

这篇关于网站可以调用浏览器扩展吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆