Convert a ViolentMonkey script to a Chrome extension

Convert a ViolentMonkey script to a Chrome extension

The script I created in Course/movie labeling solution using Firebase and ViolentMonkey worked well on one page but randomly failed to work in another. It was caused by the page’s JavaScript code which altered the behavior of XMLHttpRequest. Because I was not able to resolve it, I decided to convert my script into a Chrome extension, which would be executed in an isolated environment.

Introduction

I have been afraid of creating an extension for years because I expected lots of effort and trouble. The reality turned out to be more comfortable. There is a comprehensive guide with lots of examples on the Chrome’s page: documentation. It’s worth going through it at least to grasp some ideas and keywords. The most important ones are listed below.

Only two files are necessary to create the simplest extension: manifest.json file and a JavaScript code. For the minimum, a third file, icon, should also be provided.

The extension can either work in browser or page mode:

  • Browser mode is for extensions that work in most or all pages and can display badges onto their icons. The extension is always available.
  • Page mode is for extensions that work only in a few sites. You have to provide a condition that enables the extension for the page (otherwise it’s grayed out). And you cannot use badges.

After clicking the icon, the extension can display a popup beneath with any content (it’s an HTML file). Beware of introducing inline JavaScript code in event handlers (onclick="...") or scripts (<script>...</script>). You’ll have to work on more or less unsafe content security policies to make them work. It’s much easier to put all code in a .js file – see below for an example.

Content script is code that will be injected into the browsed page to enable some integration with it. It can be either provided in the manifest or injected using the chrome.tabs.executeScript() method.

Background script can listen to events both from the content script and from the Chrome API.

Background script can communicate with the content script via messages.

It’s fairly easy to create the extension’s options page. Again, it’s just a HTML file and you have access to Chrome storage to save the settings.

Extensions can also change some parts of the Chrome’s interface, like extend context menu, add new keywords to the search bar, change New Tab or History page, and so on.

It is possible to test and run locally developed extensions without any need for packing, signing or publishing them.

First version – background script

I started with the ViolentMonkey script from the previous post. I removed the UserScript comments at the beginning and renamed it to background.js. I had to do something with three external scripts included in that metadata:

JS

Knowing little at the beginning and being confused by the browser and page mode, I downloaded these files and included them, along with the new script, as a background script.

I also created simple icon in four resolutions: 16px (used in the toolbar), 32px, 48px, 128px. At first, I used an online icon and logo editors but I quickly gave them up to Inkscape. I created three versions of the icon varying with details and extracted them to PNG files.

At last, I created the manifest file:

JSON
manifest.json

The name, description and version attributes are obvious.

The background attribute declares scripts that will be executed in the background.

The browser_action attribute defines the icons used in the toolbar, whereas the icons attribute lists the icons used in the Chrome extension page.

The manifest_version identifies the format of this file and should be equal to 2.

Installation

As I mentioned earlier, it’s extremely easy to run own extension in Chrome.

First, open the Extension Management page by selecting More Tools > Extensions the Chrome menu.

Then enable the Developer mode switch, click LOAD UNPACKED button and select the folder with the extension’s files.

The extension immediately appeared in the toolbar. There are also useful buttons on the Extensions page that allow to: Remove, show Errors, Reload and Disable the extension.

Just after loading the extension, I saw a popup with error:

Failed to init Database: FirebaseError: [code=permission-denied]: Missing or insufficient permissions.

Then I decided to read some documentation and review some samples. Although the cause of this problem was missing permissions to access remote scripts, the bigger issue was a wrong approach. The script was supposed to access the user’s page, so it couldn’t be run in the background script. At least not entirely.

My next approach was to run the script inside the extension’s popup. At that time I didn’t know yet what was the popup and I expected it to be displayed inside the web page the user was browsing.

So, I created a simple popup which barely imported all scripts:

HTML
popup.html

I also updated the manifest to remove the background script and add the popup:

JSON
manifest.json

Now the extension loaded correctly, but after refreshing a sample page (let it be Highbrow) and clicking the extension’s icon, a small empty popup appeared below the icon (I’d rather call it dropdown). It probably started working, but only within that small popup which had no access to the page’s DOM. As a side note, it is possible to right-click that popup and select Inspect to open the devtools in the extension’s context.

Content script

I returned to the documentation and read the rest of it and reviewed some samples. I understood I had to create a content script that would be injected in the user’s page. There were two options to inject it, by providing the configuration in manifest.json or calling chrome.tabs.executeScript. I expected the first option to inject the script (and access the database) on every page I visit, 99.9% of which were not intended by my script. I didn’t want to define the list of allowed pages either in the manifest or in other configuration or options. Instead, I assumed that the script would be injected after I press my extension’s icon. I started with as little changes as possible:

HTML
popup.html

Clicking the extension resulted with the following error:

Refused to execute inline script because it violates the following Content Security Policy directive: “script-src ‘self’ blob: filesystem:”. Either the ‘unsafe-inline’ keyword, a hash (‘sha256-0xt4Ta8so3hyvkftCUrpDxElMmx2iTHEEMmio/VzsOM=’), or a nonce (‘nonce-…’) is required to enable inline execution.

I had to update my knowledge on Content Security Policy on W3C pages. The easiest solution would be adding unsafe-inline to CSP in the manifest file, but it opens a security risk. A little longer solution was extracting the JavaScript code to a JavaScript file and linking to it in popup.html:

HTML
popup.html
JS
popup.js

Adding window.close() will make the popup disappear immediately. So its only purpose was to inject the code into the current page.

Permissions

Again, nothing happened. Instead, an error appeared in the Extension Management page:

Unchecked runtime.lastError: Cannot access contents of the page. Extension manifest must request permission to access the respective host.

So far, the extension declared no permissions. It appears I needed at least one – to access the current page. Easy thing to fix, just add to the manifest:

JSON
manifest.json

Unfortunately, the extension still did not work. The latest problem is visible in the page’s Console panel (which means that the script was correctly injected into the page):

background.js:24 Uncaught TypeError: firebase.auth is not a function

I suspected that the four js files included in popup.js were loaded asynchronously, so background.js tried to access firebase-auth.js before it was loaded. As a hacky workaround, I put the last line loading background.js after a timeout of a second, but it failed to run.

Build

The better solution was merging all dependencies into one file. I quickly created a build script that concatenated the Firebase libraries and background.js into one file called background-dist.js.

JS
build.js

I ran it by node build.js.

Finally, the popup scripts file just loaded the dist file and closed popup:

JS
popup.js

Background script again

Although that code works, there is a bit simpler and cleaner solution. But first, the name background.js is confusing, as it’s actually content script, so it should be called content.js. I renamed the files and the reference to them in the build script.

Now, instead of opening an empty popup and hiding it when the user clicks the icon, I can add a handler for that click in a background script. It is very short:

JS
background.js

This new background.js file should be included in the build script, whereas both popup.html and popup.js should be deleted (from the project and build script).

Finally, the manifest should be changed. Instead of:

JSON
manifest.json

there should be:

JSON
manifest.json

Summary

This is not the best and safest solution for an extension that works like a Greasemonkey / Tampermonkey / ViolentMonkey script. It is very quick to implement though and easy to extend.

To sum it up, the simplest Chrome extension could be created by adding the following files:

  • manifest.json
JSON
manifest.json
  • assets/logo-*.png icon files

  • background.js
JS
popup.js
  • content.js – any script that should be executed

Leave a Reply

avatar
  Subscribe  
Notify of