DEV Community

loading...

"Hacking" My Way to Automatic File Injection in Chrome Extensions

Nizar
I code to make a living, automate tedious tasks, and escape reality. I love trying to create things I wish were real; whether they are stuff I see in movies or think of.
・Updated on ・4 min read

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. The function requires the activeTab permission.

According to the chrome developer api, 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 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. The thing that caught my eye was chrome.declarativeContent api. It had everything I could ever dream of...

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...

{
  conditions: [
    new chrome.declarativeContent.PageStateMatcher({
      css: [
        "meta[property='og:site_name'][content='Medium']"
      ]
    })
  ],
  actions: [
    new chrome.declarativeContent.RequestContentScript({
      css: [
        "css/medium.com.css"
      ]
    })
  ]
}
Enter fullscreen mode Exit fullscreen mode
Check the whole code on GitHub

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.

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.

new Rule({
  conditions: [
    new Condition({
      css: [
        "meta[property='og:site_name'][content='Medium']"
      ]
    })
  ],
  cssFiles: [
    "medium.com.css"
  ]
})
Enter fullscreen mode Exit fullscreen mode
Check the whole code on GitHub

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

Special thanks to slice for his review and constructive comments.

Discussion (4)

Collapse
andyzya profile image
andyzya

Is it possible to add JS file instead of CSS file paths to the page?

Collapse
nizarmah_ profile image
Nizar Author

The script injects code directly in the webpage, so yeah it should work on JS files (but you might have to make slight tweaks to the existing code)

Collapse
chan_austria777 profile image
chan πŸ€–

Isn't it possible to add the css file in content_scripts in manifest.json to inject it on page load?

Collapse
nizarmah_ profile image
Nizar Author • Edited

Ah, yes there is. There is content_scripts where you can specify url matches, javascript, and css files. There's actually a good reason why I didn't use them, if I recall correctly.

So the reason is that you need permissions to run it. So, it requires user interaction and the user to "allow it to run" every time this page is opened. I wanted a more "automatic" injection where the user doesn't have to interact with the extension. The approach I took eliminates any need for any permissions.