Programmatically determine which content script to run - javascript

I'm attempting to build a browser extension that changes the CSS of a website (among other things).
In the manifest.json file, one is able to specify a "matches" key to limit which sites the extension has permission to modify via the content script. Right now, I'm trying to present multiple different theme options, and each has its own CSS file.
Let's say I have three themes: theme1.css, theme2.css, and theme3.css
This is my save function, simply for reference.
function save(theme) {
chrome.storage.sync.set({"theme": theme}, function() {
console.log('Set the theme to ' + theme)
main()
})
}
Currently, I have this in my manifest.json:
"content_scripts": [{
"css": ["style.css"],
"js": ["content.js"],
"matches": ["https://*.example.com/*"]
}]
How can I check the stored theme to determine which one has been selected by the user, and then apply the corresponding CSS file if the URL is, let's say, https://example.com/?
Edit: I've since tried Declarative Content, this is what I've gotten. The Javascript is being injected but the CSS does not appear to be working.
var theme1 = {
conditions: [
new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostEquals: 'example.com', schemes: ['https'] }
})
],
actions: [
new chrome.declarativeContent.RequestContentScript({
js: [
"content.js"
],
css: [
"css/theme1.css"
]
})
]
};
if (theme === "theme1") {
chrome.declarativeContent.onPageChanged.removeRules(undefined, function() {
chrome.declarativeContent.onPageChanged.addRules([theme1])
console.log("Set theme to theme1")
})
}

Related

Access remoteEntry from Module Federation in a content script

TL;DR - Can I access Module Federation remotes from within a content script of a chrome extension?
I'm currently developing a Chrome extension and faced the problem that can be represented by the following.
Let's say I have 2 applications - extension and actionsLogger.
These applications are linked via Webpack Module Federation like that:
actionsLogger/webpack.js
{
...,
plugins: [
new ModuleFederationPlugin({
name: 'actionsLogger',
library: { type: 'var', name: 'actionsLogger' },
filename: 'remoteEntry.js',
exposes: {
'./logClick': './actions/logClick',
}
}),
],
devServer: {
port: 3000,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization'
},
static: {
directory: path.resolve(__dirname, 'dist'),
}
},
...
}
extension/webpack.js
{
...,
plugins: [
new webpack.ProvidePlugin({
browser: 'webextension-polyfill'
}),
new ModuleFederationPlugin({
name: 'extension',
remotes: {
actionsLogger: 'actionsLogger#http://localhost:3000/remoteEntry.js',
},
}),
],
...
}
So, as you can see, actionsLogger is running on port 3000 and extension is referring to it via Module Federation. actionsLogger contains a simple function to get position of a cursor in case of a click event.
actionsLogger/actions/logClick.js
function logClick(event) {
return { X: event.clientX, Y: event.clientY };
}
export default logClick;
Other application - extension, contains all the code for the chrome extension together with this particular script that imports logClick from actionsLogger/logClick and sends the position of a cursor to the background page whenever a click happens:
extension/tracker.js
import('actionsLogger/logClick').then((module) => {
const logClick = module.default;
document.addEventListener("click", (event) => {
const click = logClick(event);
chrome.runtime.sendMessage(click);
});
});
So manifest.json in extension looks like this:
extension/manifest.json
{
...
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["tracker.js"]
}],
...
}
And here comes the problem. If I try to open some web page with the extension installed and running, I get the following error:
Uncaught (in promise) ScriptExternalLoadError: Loading script failed.
(missing: http://localhost:3000/remoteEntry.js)
while loading "./logClick" from webpack/container/reference/actionsLogger
at webpack/container/reference/actionsLogger (remoteEntry.js":1:1)
at __webpack_require__ (bootstrap:18:1)
at handleFunction (remotes loading:33:1)
at remotes loading:52:1
at Array.forEach (<anonymous>)
at __webpack_require__.f.remotes (remotes loading:15:1)
at ensure chunk:6:1
at Array.reduce (<anonymous>)
at __webpack_require__.e (ensure chunk:5:1)
at iframe.js:44:1
First of all, I thought that I misconfigured something in my Module Federation settings, but then I tried the following - added inject.js:
extension/inject.js
const script = document.createElement('script');
script.src = "chrome-extension://ddgdsaidlksalmcgmphhechlkdlfocmd/tracker.js";
document.body.appendChild(script);
Modified manifest.json in extension:
extension/manifest.json
{
...
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["inject.js"]
}],
"web_accessible_resources": [
"tracker.js",
]
...
}
And now Module Federation works fine, but since extension/tracker.js imports import('actionsLogger/logClick') from a remote, some websites that have content security policy defined might block this request (e.g. IKEA). So this approach also won't always work.
And the initial problem with accessing MF module from content script probably happens because content scripts are running in their own isolated environment and module federation can't resolve there properly. But maybe there are some configuration flags/options or some other/better ways to make it work?
Would appreciate any advice.
Of course, minutes after I posted the question I came up with a working solution. So, basically, I decided to use the second approach where extension/tracker.js is injected via <script /> tag on a page by extension/inject.js and for the CSP issue I've added a utility that blocks a CSP header on a request for the page.
manifest.json
{
"permissions": [
...,
"webRequest",
"webRequestBlocking",
"browsingData",
...
]
}
extension/disableCSP.js
function disableCSP() {
chrome.browsingData.remove({}, { serviceWorkers: true }, function () {});
const onHeaderFilter = { urls: ['*://*/*'], types: ['main_frame', 'sub_frame'] };
browser.webRequest.onHeadersReceived.addListener(
onHeadersReceived, onHeaderFilter, ['blocking', 'responseHeaders']
);
};
function onHeadersReceived(details) {
for (let i = 0; i < details.responseHeaders.length; i++) {
if (details.responseHeaders[i].name.toLowerCase() === 'content-security-policy') {
details.responseHeaders[i].value = '';
}
}
return { responseHeaders: details.responseHeaders };
};
export default disableCSP;
And then just call disableCSP() on the background script.
I'm a bit unsure about security drawbacks of such an approach, so feel free to let me know if a potential high risk vulnerability is introduced by this solution.

Glyphicon's boostrap 3 not working in chrome extensions?

I'm start learning create an extension in chrome. I had a problem that Chrome does not accept any glyphicon icon using in boostrap version 3, like this:
<span class="glyphicon glyphicon-arrow-right"></span>
I tried config bootstrap in content.js file or call it in html's file but it still didn't work. Here are some in my mini project:
Chrome does not recognize icon.
.
In my source code manifest.json, I have configured chrome to receive bootstrap:
"web_accessible_resources": [
"/assets/*",
"/css/*",
"/js/DomAnalys/*"
],
"content_scripts": [
{
"matches": [
"https://*/*",
"http://*/*"
],
"all_frames": true,
"js": [
"/assets/js/jquery-3.4.1.min.js",
"/assets/js/bootstrap-3.4.1.min.js",
"/js/content.js"
],
"css": []
}
]
I also configured embedded boostrap directly into the js file. But
nothing happened. Here is my content.js file, i attached boostrap to
shadow dom:
const urlCssContent = chrome.extension.getURL("/css/content-script.css");
const urlCssBoostrap = chrome.extension.getURL("/assets/css/bootstrap.min.css");
const shadow = document.querySelector('#popup-modal-transl').attachShadow({
mode: 'open'
});
const importCss = `<style>
#import "${urlCssContent}";
#import "${urlCssBoostrap}";
</style>`;
shadow.innerHTML = `${importCss}${content}`;

chrome extension inject javascript at top of head before any other js

What I want to do.
Using a Chrome extension I need to inject a script into the page context in such a way that the injected script runs before any other javascript on the page.
Why do I need this?
I need to hijack all console commands for a specific page so my extension can listen to the messages.
My current issue
Currently I am catching some of the messages logged by the page but not all of them, specifically, all messages from web-directory-132a3f16cf1ea31e167fdf5294387073.js are not being caught. After some digging I discovered that the web-directory-132a3f16cf1ea31e167fdf5294387073.js is also hijacking console but doing so before my script has a chance to.
As a visual, if I look at the network tab after loading the page, I see this:
My injected script is consoleInterceptor.js. It correctly captures the output of the js files that are loaded here accept web-directory-132a3f16cf1ea31e167fdf5294387073.js
Inside of web-directory-132a3f16cf1ea31e167fdf5294387073.js is some code something like this:
....
_originalLogger: t.default.Logger,
...
// do stuff with logging ....
this._originalLogger[e].apply(this._originalLogger, s),
What I think the problem is
It seems to me that, web-directory-132a3f16cf1ea31e167fdf5294387073.js is grabbing the standard console functions and storing them internally before my script has had a chance to replace them with my own versions. So even though my script works, the web-directory-132a3f16cf1ea31e167fdf5294387073.js still uses the original standard console functions it saved.
Note that web-directory-132a3f16cf1ea31e167fdf5294387073.js is an ember application and I dont see any simple way to hook into that code to overwrite those functions too but Im open to that as a solution.
My current code:
manifest.js
...
"web_accessible_resources": [
"js/ajaxInterceptor.js",
"js/consoleInterceptor.js"
],
"version" : "5.2",
"manifest_version": 2,
"permissions": [
"<all_urls>",
"tabs",
"activeTab",
"storage",
"webNavigation",
"unlimitedStorage",
"notifications",
"clipboardWrite",
"downloads",
"tabCapture",
"cookies",
"browsingData",
"webRequest",
"*://*/*",
"gcm",
"contextMenus",
"management"
],
"externally_connectable": {
"matches": ["*://apps.mypurecloud.com/*","*://*.cloudfront.net/*"]
},
...
background.js
var options = {url: [{hostContains: 'apps.mypurecloud.com'}]};
chrome.webNavigation.onCommitted.addListener(function(details) {
// first inject the chrome extension's id
chrome.tabs.executeScript(details.tabId, {
code: "var chromeExtensionId = " + JSON.stringify(chrome.runtime.id)
});
// then inject the script which will use the dynamically added extension id
// to talk to the extension
chrome.tabs.executeScript(details.tabId, {
file: 'js/injectConsoleInterceptor.js'
});
},
options
);
chrome.runtime.onMessageExternal.addListener(
function(msg, sender, sendResponse) {
if(msg.action === 'console_intercepted'){
_this.processConsoleMessage(sender, msg.details.method, msg.details.arguments);
}
});
injectConsoleInterceptor.js
var interceptorScript = document.createElement('script');
interceptorScript.src = chrome.extension.getURL('js/consoleInterceptor.js');
interceptorScript.onload = function(){this.remove();};
(document.head || document.documentElement).prepend(interceptorScript);
consoleInterceptor.js
if(!window.hasConsoleInterceptor){
window.hasConsoleInterceptor = true;
console.log('overriding console functions');
var originals ={};
var console = window.console;
if (console){
function interceptConsoleMethod(method){
originals[method] = console[method];
console[method] = function(){
// send the data to the extension
// chromeExtensionId should be injected into the page separately and before this script
var data = {
action: 'console_intercepted',
details: {
method: method,
arguments: arguments
}
};
chrome.runtime.sendMessage(chromeExtensionId, data);
originals[method].apply(console, arguments)
}
}
// an array of the methods we want to observe
var methods = ['assert', 'count', 'debug', 'dir', 'dirxml', 'error', 'group','groupCollapsed','groupEnd','info','log', 'profile', 'profileEnd','time','timeEnd','timeStamp','trace','warn','table'];
for (var i = 0; i < methods.length; i++){
interceptConsoleMethod(methods[i])
}
console.log('Successfully overridden console functions: '+methods.join(','));
}
}
My question
What can I do to make consoleInterceptor.js run before web-directory-132a3f16cf1ea31e167fdf5294387073.js loads so that web-directory-132a3f16cf1ea31e167fdf5294387073.js uses my modified console functions rather than the default browser console funcitons?
You can try this.In manifest.json file:
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["script/inject.js"],
"run_at":"document_start"
}
]
In inject.js:
var ss = document.createElement("script");
ss.innerHTML= "xxx";
document.documentElement.appendChild(ss);

Export as Excel file not working for HTML table in Chrome Extension

I am currently developing an chrome extension for my university and there is a problem i am facing, the 'Export as Excel' does not show up after i add jquery code in manifest.json as well as in timetable.js (Look below for more info)
Here is my code :
Manifest.json
{
"name": "VIT Academics Enhancement Suite",
"version": "0.1",
"author": "Rahul Kapoor",
"description": "Extension to help you improve your experience on VIT Academics",
"permissions": ["*://academics.vit.ac.in/*"],
"content_scripts": [
{
"matches": ["*://academics.vit.ac.in/parent/parent_login.asp","*://academics.vit.ac.in/student/stud_login.asp","*://academics.vit.ac.in/parent/","*://academics.vit.ac.in/student/","*://27.251.102.132/parent/parent_login.asp","*://27.251.102.132/student/stud_login.asp","*://27.251.102.132/parent/","*://27.251.102.132/student/"],
"js": ["captcha.js"]
},
{
"matches": ["*://academics.vit.ac.in/student/timetable_ws.asp"],
"all_frames": true,
"js": ["timetable.js"],
"js": ["jquery.js"]
},
{
"matches": ["*://academics.vit.ac.in/*/attn_report.asp*","*://academics.vit.ac.in/student/attn_report_details.asp","*://27.251.102.132/*/attn_report.asp*"],
"all_frames": true,
"js": ["attendance.js"],
"css": ["home.css"]
}
],
"manifest_version": 2
}
Timetable.js
var textbox = document.getElementsByName('regular')[0];
console.log(textbox);
var para = document.createElement("input");
var t = document.createTextNode("Show Password");
para.setAttribute("type", "button");
para.setAttribute("id", "btnExport");
para.setAttribute("value","Export Table data into Excel");
textbox.parentElement.appendChild(para);
$("#btnExport").click(function (e) {
window.open('data:application/vnd.ms-excel,' + $('table:nth-child(1)').html());
e.preventDefault();
});
The table HTML code has no id or class so i am targeting it using nth-child but as soon i add jquery.js in manifest which as CDN library code then the button 'Export as Excel' disappears.
Am i doing it right or is it something else i need to do, Please help.
Your problem is in the way you specify multiple script dependencies in the manifest:
"js": ["timetable.js"],
"js": ["jquery.js"]
In JSON, the latter entry will simply overwrite the former, so now you no longer reference timetable.js
The value for the "js" key is already an array (as denoted by the [ ]), so you just add your dependencies to that array:
"js": ["timetable.js", "jquery.js"]
I'm not sure in what order the scripts are injected, you might need to switch them around.

chrome.tabs.executeScript: How to get access to variable from content script in background script?

How to get access to variable app from content script app.js in background script background.js?
Here is how I try it (background.js):
chrome.tabs.executeScript(null, { file: "app.js" }, function() {
app.getSettings('authorizeInProgress'); //...
});
Here is what I get:
Here is manifest.json:
{
"name": "ctrl-vk",
"version": "0.1.3",
"manifest_version": 2,
"description": "Chrome extension for ctrl+v insertion of images to vk.com",
"content_scripts": [{
"matches": [
"http://*/*",
"https://*/*"
],
"js": ["jquery-1.9.1.min.js"
],
"run_at": "document_end"
}],
"web_accessible_resources": [
"jquery-1.9.1.min.js"
],
"permissions" : [
"tabs",
"http://*/*",
"https://*/*"
],
"background": {
"persistent": false,
"scripts": ["background.js"]
}
}
Full code for instance, at github
https://github.com/MaxLord/ctrl-vk/tree/with_bug
To avoid above error use following code
if (tab.url.indexOf("chrome-devtools://") == -1) {
chrome.tabs.executeScript(tabId, {
file: "app.js"
}, function () {
if (app.getSettings('authorizeInProgress')) {
alert('my tab');
REDIRECT_URI = app.getSettings('REDIRECT_URI');
if (tab.url.indexOf(REDIRECT_URI + "#access_token") >= 0) {
app.setSettings('authorize_in_progress', false);
chrome.tabs.remove(tabId);
return app.finishAuthorize(tab.url);
}
} else {
alert('not my');
}
});
}
instead of
chrome.tabs.executeScript(null, {
file: "app.js"
}, function () {
if (app.getSettings('authorizeInProgress')) {
alert('my tab');
REDIRECT_URI = app.getSettings('REDIRECT_URI');
if (tab.url.indexOf(REDIRECT_URI + "#access_token") >= 0) {
app.setSettings('authorize_in_progress', false);
chrome.tabs.remove(tabId);
return app.finishAuthorize(tab.url);
}
} else {
alert('not my');
}
});
Explanation
chrome://extensions/ page also fires chrome.tabs.onUpdated event, to avoid it we have to add a filter to skip all dev-tool pages.
(Would've submitted this as comment to the accepted answer but still lack the required reputation)
You should also give the tabId to chrome.tabs.executeScript as first argument when you have it. Otherwise you risk user switching windows/tabs right after requesting a URL and background.js doing executeScript against wrong page.
While fairly obvious on hindsight it threw me for a loop when I got that same error message "Cannot access contents of url "chrome-devtools://.." even though my chrome.tabs.onUpdated eventhandler was checking that the page user requested had some specific domain name just before doing the executeScript call.
So keep in mind, chrome.tabs.executeScript(null,..) runs the script in active window, even if the active window might be developer tools inspector.
We should notice that, in the manifest cofigļ¼š
"content_scripts": [{
"matches": [
"http://*/*",
"https://*/*"
],
"js": ["jquery-1.9.1.min.js"
],
in the "matches" part, only http, https are matched, so if you load your extension in page like: 'chrome://extensions/', or 'file:///D:xxx', that error will occur.
You may load your extension in the page with the url 'http://'; or add more rules in your 'matches' array.

Categories