"Hacking" My Way to Automatic File Injection in Chrome Extensions
Tags: css, javascript, chromeextension, showdev
Published: 2020-04-13 02:41:39

A couple of days ago, it got to my attention that most websites where I read my articles have bad printable pages. So, I decided to make a chrome extension that "prettifies" the print preview for some popular websites that I print.

The idea was to write a certain set of rules and/or conditions, which once satisfied tell the extension which CSS file improves the _print media_ for that page.

Among those websites were multiple Medium based websites such as TowardsDataScience, Medium, etc.. I had to find a solution that wasn't only _url_ or _host_ specific; otherwise, I would end up having to enter _every_ Medium based website's _url_ or _host_.

Thus, the solution was to check if a certain element existed in the page using a CSS _selector_, which meant that I had to be able to get the page's HTML source first... Moreover, the CSS _print media_ file needed to be later on _injected_ to the page automatically.

However, _injecting_ the CSS file _programmatically_ is done through [`chrome.tabs.insertCSS`](https://developer.chrome.com/extensions/tabs#method-insertCSS). The function requires the [`activeTab` permission](https://developer.chrome.com/extensions/activeTab).

According to the [chrome developer api](https://developer.chrome.com/extensions/activeTab#invoking-activeTab), user gestures (through clicking a certain action or item, or using a keyboard shortcut command) are necessary to enable `activeTab`. 

Yet again, the file needs to be _injected_ automatically.

**This post will be explaining how I managed to inject CSS files automatically without using `activeTab` permission or `chrome.tabs.insertCSS`**

Here are the  approaches I took, in order:

### The Basic
The basic approach is the [`activeTab` permission](https://developer.chrome.com/extensions/activeTab) approach. Unfortunately, there is another problem to it, other than _injecting_ the CSS file.

Getting the page's HTML source is not possible because the chrome developer api does not have any method to get a certain tab's HTML document. Consequently, I had to _inject_ JS to the tab to _query_ the _selector_ and check if the element exists.

This prevented me from being able to check if a CSS _selector_ matches an element in the page, or even _inject_ the CSS _print media_ file, **unless I interact with the _action_** to enable `activeTab` permission on that tab.

So clearly, I needed a different solution.

### The Fantasy

Luckily, the **fantasy** unveiled itself to me while reading their [developer guide](https://developer.chrome.com/extensions/devguide). The thing that caught my eye was  [`chrome.declarativeContent` api](https://developer.chrome.com/extensions/declarativeContent). It had everything I could ever dream of...
* [`PageStateMatcher`](https://developer.chrome.com/extensions/declarativeContent#type-PageStateMatcher) that supports CSS _matching_ and _pageUrl matching_.
* [`RequestContentScript`](https://developer.chrome.com/extensions/declarativeContent#type-RequestContentScript) that supports _injecting_ CSS and JS files **after the rules and/or conditions were satisfied**.

> Please note that this api requires the use of the `declarativeContent` permission.

So this approach would work in the following way
##### How It Works
1. Chrome checks for the rules defined in the extension
2. For every rule, if one of the conditions or `PageStateMatcher` is satisfied, then Chrome executes the actions specified in the _rule_.

So, here's the rule I **_would_** be using for Medium based websites...
```js
{
  conditions: [
    new chrome.declarativeContent.PageStateMatcher({
      css: [
        "meta[property='og:site_name'][content='Medium']"
      ]
    })
  ],
  actions: [
    new chrome.declarativeContent.RequestContentScript({
      css: [
        "css/medium.com.css"
      ]
    })
  ]
}
```
> ##### [Check the whole code on GitHub](https://github.com/nizarmah/fineprint/blob/declarativeContent.RequestContentScript/background.js)

Oh, yes **_would_** be, because according to the api that action is **not supported on stable builds of Chrome**.
> Since Chrome 38.
>
> Declarative event action that injects a content script.
> 
> **WARNING:**  This action is still experimental and is not supported on stable builds of Chrome.

_I tried so hard and got so far, but in the end it didn't even matter_

### The "Hack" Fantasy

The **fantasy** approach was just too good to go unnoticed and ignored.  It was the solution I needed, an automatic CSS file injection. Hence, I needed to implement it myself in a _hacky_ way. To implement that _hacky_ approach, I used two different apis.
* [Simple One-Time Messaging Requests](https://developer.chrome.com/extensions/messaging#simple)
* [Declaratively Injected Content Scripts](https://developer.chrome.com/extensions/content_scripts#declaratively)

> Although we are using those apis, it is important to note that no permissions are needed in `manifest.json`

Additionally, the rules and/or conditions are defined in a similar way to `chrome.declarativeContent`.
```js
new Rule({
  conditions: [
    new Condition({
      css: [
        "meta[property='og:site_name'][content='Medium']"
      ]
    })
  ],
  cssFiles: [
    "medium.com.css"
  ]
})
```
> ##### [Check the whole code on GitHub](https://github.com/nizarmah/fineprint/blob/master/page_rules.js)

So here's how the hacky implementation worked.

##### Thought Process
1. _`injector.js`_ that is loaded to all pages (`<all_urls>`).
2. _`injector.js`_ sends to the extension
	* page `window.location`
	* page `document` object
3. Extension's _`background.js`_ receives the message from the page
4. Extension's _`validator.js`_ checks if any rules and/or conditions satisfies the page depending on:
	* _pageUrl_ _matching_
	* CSS _matching_ by _selector_
5. If a rule satisfies a page, the extension's _`background.js`_ sends back the _path_ of all the `cssFiles` associated with that `Rule`.
6. _`injector.js`_ receives the response from the extension's _`background.js`_ and adds the CSS file paths to the page.

> ##### Please note that there are multiple `manifest.json` attributes that need to be added in order to make all that possible. However, for the sake of keeping this short, **the code is commented in detail on GitHub**.
> ##### [Check the whole implementation on GitHub](https://github.com/nizarmah/fineprint)

_Special thanks to [slice](https://slice.zone/) for his review and constructive comments._