How to implement message passing callbacks in an all-in-one (Edge/Firefox/Chrome) browser extension’s content script?

Development Environment OS: Windows 7 Enterprise LTS
Browser compatibility minimum requirements: Should support all Edge, Firefox, Chrome browsers, as of 2018.
Current ongoing issue: Unable to run VM on dev workstation; Cannot run Windows 10 VMs to debug Microsoft Edge extensions.

To explain:

  • An “all-in-one browser extension” refers to a browser extension code that uses the same code with minor differences to work on various WebExtensions / Chrome Extensions supported browsers. At bare minimum, the same codebase should work and run on Edge, Firefox, and Chrome with very minor changes.
  • Callbacks on the content scripts for Edge/Firefox/Chrome extensions are handled differently.
  • For unknown reasons, I cannot run VM on my workstation machine. When VM is running, VM client is black. This is a localized issue on my end that I cannot resolve, so I’m forced to find a different solution/alternative.

How are they handled differently on the content scripts:

  • Edge: browser.runtime.sendMessage uses callbacks, and returns undefined.
  • Firefox: browser.runtime.sendMessage uses Promises, and returns a Promise.
  • Chrome: chrome.runtime.sendMessage uses callbacks, and returns undefined.

According to various references:

Firefox / Chrome / MS Edge extensions using chrome.* or browser.*

https://www.smashingmagazine.com/2017/04/browser-extension-edge-chrome-firefox-opera-brave-vivaldi/

On the content scripts, you can declare the following JavaScript snippet at the top in order to create a global variable that can be referenced everywhere else:

//Global "browser" namespace definition.
window.browser = (function() {
    return window.msBrowser || window.browser || window.chrome;
})();

Unfortunately, because of the issue I’m experiencing (VM not running), I cannot tell if window.msBrowser is still being used. And this solution is not helpful for me when handling message callbacks when using namespace.runtime.sendMessage.


With all that said, my main question is: How to write a message passing function that can handle callbacks properly?

Currently, I’m using the following code:

function sendGlobalMessage(messageRequest, callback) {
    if (chrome && window.openDatabase) {
        //This is Chrome browser
        chrome.runtime.sendMessage(messageRequest, callback);
    }
    else if (browser) {
        try {
            //Edge will error out because of a quirk in Edge IndexedDB implementation.
            //See https://gist.github.com/nolanlawson/a841ee23436410f37168
            let db = window.indexedDB.open("edge", (Math.pow(2, 30) + 1));
            db.onerror = function(e) {
                throw new Error("edge is found");
            };
            db.onsuccess = function(e) {
                //This is Firefox browser.
                browser.runtime.sendMessage(messageRequest).then(callback);
            };
        }
        catch (e) {
            //This is Edge browser
            browser.runtime.sendMessage(messageRequest, callback);
        }
    }
}

I truly felt this is a hacky solution, because the code is based off of browser platform exclusive quirks in order to separate chrome.runtime.sendMessage and browser.runtime.sendMessage API calls, so as to handle callbacks in their respective platforms. I really wanted to change this.

So I’m asking what better ways are there, out there, that is useful to detect the different platforms, and handle message passing callbacks properly at the same time?

Thanks in advance.

Clearing HTTP Basic Auth using a web extension

I am working on a web extension that displays a badge as a ‘page action’ when you are logged in using HTTP Basic auth.

When clicked, I would to automatically log the user out. I can’t seem to find an API that allows me to clear the HTTP Basic auth cache. Does such an API exist?

Notification not shown in Chrome extension using Mozilla’s webextension-polyfill

I just started with developing a Firefox add-on. It works fine in Firefox so I’d like to make it “compatible” to a Coogle Chrome extension.

For this I inject Mozilla webextension-polyfill and basically the add-on also runs in Chrome. There is one thing however I can’t get to work…

In Firefox a notifcation is shown to the user if the content script sends a message which is received by the background script. Running this in Chrome results in the following exception:

Uncaught (in promise) 
{message: "The message port closed before a response was received."}
callbackArgs @ VM18 browser-polyfill.js:630
sendResponseAndClearCallback @ VM29 extensions::messaging:417
disconnectListener @ VM29 extensions::messaging:441
EventImpl.dispatchToListener @ VM19 extensions::event_bindings:403
publicClassPrototype.(anonymous function) @ VM25 extensions::utils:138
EventImpl.dispatch_ @ VM19 extensions::event_bindings:387
EventImpl.dispatch @ VM19 extensions::event_bindings:409
publicClassPrototype.(anonymous function) @ VM25 extensions::utils:138
dispatchOnDisconnect @ VM29 extensions::messaging:378

I can tell that this comes from the webextension-polyfill but I cannot find a way so that the notification is also shown in Chrome.

Here are the relevant code snippets…

manifest.json

{
  "manifest_version": 2,
  // ...
  "background": {
    "scripts": [
      "lib/browser-polyfill.js",
      "background-script.js"
    ],
    "persistent": false
  },

  "options_ui": {
    "page": "settings/options.html"
  }
}

background-script.js

function notify(message) {
    if (message.copied) {
        browser.notifications.create({
           "type": "basic",
           "title": "Notifaction title",
           "message": "Hello, world!"
        });
    }
}

browser.browserAction.onClicked.addListener(() => {
    browser.tabs.executeScript({file: "lib/browser-polyfill.js"});
    browser.tabs.executeScript({file: "content-script.js"});
});

browser.runtime.onMessage.addListener(notify);

content-script.js

browser.storage.local.get({elementId: ""})
    .then(() => {
        browser.runtime.sendMessage({copied: true});
    });

TypeError: browser is undefined (Web Extension Messaging)

I am trying to communicate my web page script with my content script of my web extension with the code below

Web Page Script

const browser = window.browser || window.chrome;
browser.runtime.sendMessage(message,
     function (response) {
          console.log(response);
     }
);

However, I keep getting the error TypeError: browser is undefined. The same goes if I use chrome.runtime.sendMessage() instead.

How am I supposed to use this method?

How to reload background scripts?

According to MDN,

Background scripts are loaded as soon as the extension is loaded and stay loaded until the extension is disabled or uninstalled.

My background script changes its behavior accordingly user’s options, stored on localStorage. When the user change options (using options_ui), localStorage is updated, but since the background script stay loaded, user options are not honored. If the browser is reloaded, options are honored.

How do I reload the background script after the user change options?

Uncaught (in Promise) Error When Using Polyfill for Porting Firefox Extensions to Chrome

I wrote a web extension for Firefox Quantum. The extension has a popup, which when clicked provides a 3 button menu. When one of the buttons is clicked, the extension will inject a DIV containing an iFrame into the user’s current page.

Firefox extensions use the promise API and use the browser namespace instead of “callbacks” and the chrome namespace. As such, Mozilla provides a polyfill to allow easy porting of code written for Firefox Extensions.

Excerpt from this article.

If you do write your extension to use browser and promises, then Firefox also provides a polyfill that will enable it to run in Chrome: https://github.com/mozilla/webextension-polyfill.

I followed the tutorial on github to modify the html file for my popup (I removed the CSS classes to make the example more concise):





    
    
    



    

Then in my “choose_length_page.js” file, I have the following code:

//Enable the polyfill for the content script and execute it in the current tab

browser.tabs.executeScript({ file: "../polyfills/browser-polyfill.js" }).then(loadContentScript);

function loadContentScript() {
    browser.tabs.executeScript({ file: "../inject-content/inject.js" }).then(listenForClicks);
}

function listenForClicks() {
    document.addEventListener('click', e => {

        if (!e.target.classList.contains('btn')) {
            return;
        } else {
            browser.tabs.query({ active: true, currentWindow: true })
                .then(tabs => {
                    browser.tabs.sendMessage(tabs[0].id, { summaryLength: e.target.id, targetURL: tabs[0].url });
                });
        }
    });
}

However, when I run this code I get the following error (“choose_length_page.html” is the HTML file in this post):

Error

Why is this error happening, and how can I fix it?

Web Extension – Content Security Policy Error when Iframe Source Executes Script

I have a chrome/firefox web extension that uses a content script to inject HTML into a webpage when a button is clicked. The HTML that is injected consists of an iFrame nested within several divs.

Here’s the relevant portion of the content script:

var iFrame = document.createElement("iFrame");
iFrame.id = "contentFrame";
iFrame.style.cssText = "width: 100%; height: 100%; border: none;";
iFrame.src = browser.extension.getURL("inject-content/inject.html");

var boxDiv = document.createElement("div");
boxDiv.style.cssText = "left: calc(100% - 390px); position: fixed; top: 0px; width: 390px; z-index: 1;"

var zeroDiv = document.createElement("div");
zeroDiv.style.cssText = "position: fixed; width: 0px; height: 0px; top: 0px; left: 0px; z-index: 2147483647;";

var outerDiv = document.createElement("div");
outerDiv.id = outerDivID;

boxDiv.appendChild(iFrame);
zeroDiv.appendChild(boxDiv);
outerDiv.appendChild(zeroDiv);
document.body.appendChild(outerDiv);

As indicated by the code, the source of the iFrame is a file called “inject.html”. Two important features of inject.html are:

1) A script tag (inside the header) that refers to the file for a javascript library in the same directory.


2) A piece of inline javascript that uses “perfect-scrollbar.js”. Also, for reference, here is the library itself: https://github.com/utatti/perfect-scrollbar


When I directly open the file from my computer (i.e – right-click, open with Chrome), it works fine. However, when I use my extension in Firefox, I get the following error:

Content Security Policy: The page’s settings blocked the loading of a resource at self (“script-src”).

Source:
console.log(“hello world”);

va….

I read through the documentation and it seems like in-line javascript is not allowed by the default content security policy.

Relevant documentation:

The default content security policy for extensions is:

"script-src 'self'; object-src 'self';"

This will be applied to any extension that has not explicitly set its own content security policy using the content_security_policy manifest.json key. It has the following consequences:

You may only load and resources that are local to the extension.

The extension is not allowed to evaluate strings as JavaScript.

Inline JavaScript is not executed.

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#Unsafe_inline_script

I would solve the problem of inline javascript by using import statements, but according to Mozilla, those are not supported in Firefox right now: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import

According to the documentation, it is possible to allow some inline Javascript by creating a sha-256 hash of your script.

https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/content_security_policy

Allow the extension to execute inline scripts, by supplying the hash of the script in the “script-src” directive.

Allow the inline script: :

"content_security_policy": "script-src 'self' 'sha256-qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng='; object-src 'self'"

and this

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#Unsafe_inline_script

Alternatively, you can create hashes from your inline scripts. CSP supports sha256, sha384 and sha512.

Content-Security-Policy: script-src 'sha256-B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8='

When generating the hash, don’t include the

Firefox extension example for global variable?

I’m looking for a way to set a global window variable from an extension that can be accessed from a site’s scripts.

Eg.

[Extension]
window.test = 1;

[Page]
console.log(test); // 1

I didn’t come across this in the mdn webextensions examples github: https://github.com/mdn/webextensions-examples

What’s the easiest way to set this up with access permissions?

WebExtensions: Is it possible to intercept file opening?

I want to create a web extension for Firefox, Chrome etc., and I wonder is it possible to create a custom event handler, which would listen the event of file opening?

In other words, I want the following functionality:

  1. A user opens a file from the local disk with a browser (e.g. .rtf)
  2. A browser fires the event, which is intercepted by my listener.
  3. The extension opens a new tab where a result (e.g. the file representation) is shown.

The main problem is that I don’t know whether there is such event at all and I found nothing about it in the documentation at MDN. But as I remember browsers can open .pdf file with double click, so maybe it’s possible to open not only .pdf?

Web Extension – document.getElementById returning null before execution of async method

I have a chrome/firefox web extension. When clicked, this web extension appends a DIV, which contains several other DIVs and a iFrame in a nested structure, to the inside of the element in a website’s HTML file. It does this through the usage of a content script.

Here is the simplified code of the onMessage listener in the content script:

    browser.runtime.onMessage.addListener((message) => {

    var iFrame = document.createElement("iFrame");
    iFrame.id = "contentFrame";
    iFrame.style.cssText = "width: 100%; height: 100%; border: none;";
    iFrame.src = browser.extension.getURL("inject.html");

    var boxDiv = document.createElement("div");
    boxDiv.style.cssText = "background: white; box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 9px 8px; height: 100%; left: calc(100% - 390px); position: fixed; top: 0px; width: 390px; z-index: 1;"

    var zeroDiv = document.createElement("div");
    zeroDiv.style.cssText = "position: fixed; width: 0px; height: 0px; top: 0px; left: 0px; z-index: 2147483647;";

    var outerDiv = document.createElement("div");

    boxDiv.appendChild(iFrame);
    zeroDiv.appendChild(boxDiv);
    outerDiv.appendChild(zeroDiv);
    document.body.appendChild(outerDiv);

    var closeButton1 = document.getElementById("contentFrame").contentWindow.document.getElementById("close-btn");
    console.log("Close Button1 Fetched: " + closeButton1);

    //closeButton.onClick = function () {
    //  console.log("Close button clicked");
    //}

    returnSummary(message.summaryLength, message.targetURL).then(summary => {

        var closeButton2 = document.getElementById("contentFrame").contentWindow.document.getElementById("close-btn");
        console.log("Close Button2 Fetched, Setting Listener: " + closeButton2);

        closeButton2.onClick = function() {
            console.log("Close button clicked");
        }
    });
});

For context – returnSummary is an async function that contains a bunch of fetch requests.

The odd part is as follows:

The first log statement: console.log("Close Button1 Fetched: " + closeButton1); returns null,

but the second log statement: console.log("Close Button2 Fetched, Setting Listener: " + closeButton2); returns [object HTMLButtonElement].

Thus, the document.getElementbyId seems to be working after the async request finishes. Why is this happening, and how can I retrieve the value of the HTML element with the ID close-btn as early as possible? (I want to set an onClick listener on the button).

Another odd problem is that even when I set the onClick method of the object closeButton2, clicking on the “close button” does nothing. But, that’s not the primary issue of this post.

Also here is the HTML inside “inject.html”, (the top is mostly just CSS):






    
    



    

Loading Summary...