How to use Web Workers with Webextensions? - javascript

I'm trying to use a web worker in my webextension to make things run faster, but I can't seem to get even the demo webworker example that MDN provides to work. This is my manifest:
{
"manifest_version": 2,
"name": "test",
"version": "1.0.0",
"content_scripts": [
{
"matches": [
"http://*/*",
"https://*/*"
],
"js": [
"sharedTest.js"
]
}
]
}
This is sharedTest.js:
console.log("js loaded");
if (window.Worker) { // Check if Browser supports the Worker api.
// Requires script name as input
var myWorker = new Worker(browser.runtime.getURL('testWorker.js'));
myWorker.onmessage = function(e) {
console.log('Message received from worker');
console.log(e.data);
};
myWorker.postMessage([42, 23]); // Sending message as an array to the worker
console.log('Message posted to worker');
}
and this is testWorker.js:
onmessage = function(e) {
console.log('Message received from main script');
var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
console.log('Posting message back to main script');
postMessage(workerResult);
}
If anyone can tell me what I'm doing wrong or if there is anything missing in my manifest, that'd be great.

So it turns out, due to the same-origin policy, you can't use normal web workers in webextensions. However, you can get around this by wrapping your worker in a blob like so:
var blob = new Blob([
`onmessage = function(e) {
console.log('Message received from main script');
var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
console.log('Posting message back to main script');
postMessage(workerResult);
}`]);
// Obtain a blob URL reference to our worker 'file'.
var blobURL = window.URL.createObjectURL(blob);
var worker = new Worker(blobURL);
worker.onmessage = function(e) {
console.log('Message received from worker');
console.log(e.data);
};
worker.postMessage([42, 23]); // Sending message as an array to the worker
console.log('Message posted to worker');
And that will work. The above example was based on this article, and that does work in webextensions (for theoretically everything but Internet Explorer). However, that blob worker is not within the same origin as the rest of your extension anymore, so due to the same-origin policy, the global importScripts() will not work to import any files in your extension. This means that you have to include all necessary methods and logic within the Blob, which is less than ideal.
Alternatively, I realized that this is kind of what background scripts are for, or for my case, event pages.

It may be an old question, but in case anyone is looking for a solution other than using blobs or background scripts, this might be it - without caveats.
If you attempt to load your WebWorker with your current manifest.js, you will get an error. However, you can add your worker.js file as an additional resource, so that your browser can access it. You can do this by adding web_accessible_resource key to your manifest.json. NB: the path specified as value to the key will be relative to the manifest file.
{
"manifest_version": 2,
"name": "MyExtension",
"version": "1.0",
"content_scripts": [
{
"all_frames": true,
"matches": [ "<all_urls>" ],
"js": [ "js/content.js", "js/import.js" ],
"run_at": "document_end",
"css": ["css/style.css"]
}
],
"web_accessible_resources": [ "js/worker.js", "pictures/cat.svg" ]
}
You can then obtain the runtime url of your worker from your content.js to create the worker:
const worker = new Worker(browser.runtime.getURL('js/worker.js'));
worker.onmessage = (e) => {window.console.log(e);
To import a script to your worker, you first need to specify it as a content script in your manifest (path relative to manifest). Then in your worker you can import it as:
self.importScripts('import.js');
self.onmessage = (e) => importStuff.doWork(e);
The path to your import script here is relative to the worker file itself.
That's it - enjoy threading in extensions.

It appears to be possible to create Workers from the background page! While the original question looked like they were trying to use a content script that feature does not seem to be available.
Here's a dumb unimaginative example for spawning a web worker on the background page:
manifest.json:
{
"manifest_version": 2,
"name": "",
"description": "",
"version": "0.0.1",
"background": { "page": "background.html" }
}
background.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="background.js"></script>
</head>
</html>
background.js:
if(window.Worker)
{
const myWorker = new Worker(browser.runtime.getURL('worker.js'));
myWorker.onmessage = function(msg) {
console.log("Main Script: Message received", msg.data);
}
let i = 0;
setInterval(() => {myWorker.postMessage({num: i++});}, 500);
}
else
{
console.log("Worker's not supported :(")
}
worker.js:
onmessage = function(msg)
{
postMessage({timestamp: Date.now(), passedData: msg.data});
}
If you need to use a content script then you can use ports to send messages from the script to the background page and back again.

Related

Chrome Extension - How to interact between the main extension popup (default_popup) and a page script (web_accessible_resources)

I created an extension, where I placed a script into a page markup and performs some actions according to the following article:
var s = document.createElement('script');
s.src = chrome.runtime.getURL('code.js');
s.onload = function() {
this.remove();
};
(document.head || document.documentElement).appendChild(s);
I also displayed a checkbox in "default_popup" which should indicate whether to execute a part of methods in script from "code.js" (web_accessible_resources) or not.
However, I have no idea how to interact between the script from "content_scripts" (which has access to "default_popup") and the script from "web_accessible_resources".
Could you suggest something?
I understand that I can completely replace the "web_accessible_resources" script, but this does not seem to be the best practice.
Thank you.
This is a sample of communication between popup and tab.
manifest.json
{
"name": "content_scripts + popup",
"version": "1.0",
"manifest_version": 3,
"content_scripts": [
{
"js": [
"matches.js"
],
"matches": [
"<all_urls>"
]
}
],
"host_permissions": [
"<all_urls>"
],
"action": {
"default_popup": "popup.html"
}
}
popup.js
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
console.log("Send");
chrome.tabs.sendMessage(tabs[0].id, "message", (response) => {
console.log("Recv response = " + response.title);
document.getElementById("title").innerText = response.title;
});
});
matches.js
console.log("matches.js");
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log("Recv. Send response = " + document.title);
sendResponse({ title: document.title });
return true;
});
popup.html
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
* {
font-size: x-large;
}
</style>
</head>
<body style="min-width:300px">
<div id="title"></div><br>
<script src="popup.js"></script>
</body>
</html>
I'm not sure exactly what you're asking but I'm assuming you have 3 things: (1) a pop up, (2) a content script, and (3) a script injected into the page, running in page context. Firstly, to send messages to the popup, you can run chrome.runtime.sendMessage() and have a listener in your popup's script.js file with chrome.runtime.onMessage(). To get the data from the page script is different, it cannot communicate with your browser extension directly. So, from your page context script, you can create an element with a unique ID and store some data in an attribute. Use a modification of a MutationObserver to run a waitForElm() function (see this thread - How to wait until an element exists?). In your content script, you'll wait for the element to be created, collect data from the attribute you set, and then run chrome.runtime.sendMessage() as mentioned earlier to get that data to your pop up.

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);

Google Extension: Access to DOM in iframe of a different domain

So I've found several pages on here, as well as various blog posts that seem to do pretty much exactly what I want to do, but they are all a few years old and seem really easy but don't work.
As the title says, On thisdomain.com there is a iframe from thatdomain.com and I want to get the value in a div in that iframe.
Manifest.json
{
"manifest_version": 1,
"name": "MyExtention",
"version": "1.0",
"description": "Nothing Yet",
"permissions": [
"storage",
"tabs",
"unlimitedStorage",
"webRequest",
"webNavigation",
"*://*.match-both-iframe-and-main-domain.com/*",
"*://*/*"
],
"background": {
"scripts": ["listener.js"],
"persistent": true
},
"content_scripts":
[
{
"matches": ["*://*.matchnothing.shshdjdjffkdj.com/*"],
"js": ["mainscript.js"],
"all_frames": true
}
]
}
The content script url matches nothing because it is fired from a listener (which works). Basically it waits for a request from one of 2 urls before it activates.
listener.js
var chrome = chrome || {};
var callback = function(listenerRes) {
console.log(listenerRes.url);
if (listenerRes.url.indexOf("listenurl1") > -1 ||
listenerRes.url.indexOf("listenurl2") > -1) {
chrome.tabs.get(listenerRes.tabId, function(tab) {
chrome.tabs.executeScript(tab.id, {file: "mainscript.js"});
});
}
};
chrome.webRequest.onBeforeRequest.addListener( callback, {urls: ["*://*.google.com/*"]} );
mainscript.js
var chrome = chrome || {};
... // helper functions and such
var iframe = document.getElementsByid('myiframe');
// Get all data.
var datas = [];
try {
datas = iframe.contentWindow.document.getElementsByClassName('mydata'); // error is here
}catch(e){
console.log(e);
}
... // do stuff with the data
On the commented line it throws a "Blocked a frame with origin URL from accessing a cross-origin frame."
So I am under the impression that some combination of all_frames = true, the chrome.tabs.executeScript, and the domains in the permissions should allow for this to work. But it doesn't.
It might be important to note, the reason for this listener is because the iframe isnt on the page to start.
Please help, Im an experienced web developer but this is my 1st foray into Chrome Extentions.

Chrome Extension Resources must be listed in the web_accessible_resources manifest key

I'm trying to send a httpget request in chrome but I get this error
Resources must be listed in the web_accessible_resources manifest key
here's my button code
contentInput.onclick = function(){
var assetid = $('.thumbnail-span').attr("data-3d-url")
var baseurl = 'http://www.roblox.com'
var xhr = new XMLHttpRequest();
xhr.open("GET", chrome.extension.getURL(baseurl + assetid), true);
var result = xhr.responseText;
xhr.send();
console.log(result)
chrome.extension.sendRequest({
action: "EditContent",
type: assetType,
name: assetName,
content: contentData
})
and my manifest file
{
"name": "ROBLOX Object Downloader .obj",
"short_name": "OBJDownloader",
"description": "Allows you to quickly download assets from the browser as a .obj ",
"version": "1.0.0",
"icons": {"128":"icon.png"},
"permissions": [
"http://*.roblox.com/*",
"http://*.rbxcdn.com/*",
"downloads",
"downloads.open"
],
"background": {"scripts":["background.js"]},
"content_scripts": [
{
"matches": ["http://*.roblox.com/*-item?id=*"],
"js": ["item.js","jquery.js"]
},
{
"matches": ["http://*.roblox.com/*ser.aspx*"],
"js": ["user.js","jquery.js"]
},
{
"matches": ["http://*.roblox.com/*atalog/*"],
"js": ["cataloginsert.js","jquery.js"]
}
],
"manifest_version": 2
}
The chrome.extension.getURL function is used to get a file from your own computer located inside the extension's directory:
string chrome.extension.getURL(string path): Converts a relative path within an extension install directory to a fully-qualified URL.
This means your Ajax request is trying to access a URL like
chrome-extension://aaaaaabbbbbccccccdddddd/http://www.roblox.com/some-asset-id
In order to access files through chrome-extension://, you have to make them explicitly accessible to web pages through the web_accessible_resource manifest field.
However, you probably just want to get the web URL http://www.roblox.com/some-asset-id. In case, getURL is completely inappropriate. Simply do something like:
xhr.open("GET", baseurl + assetid, true);
Your code has an additional problem, which is that it doesn't wait for the asynchronous Ajax call to complete. You should wait for the load event and then read responseText:
contentInput.onclick = function(){
...
var xhr = new XMLHttpRequest();
xhr.open("GET", chrome.extension.getURL(baseurl + assetid), true);
xhr.addEventListener("load", function() {
var result = xhr.responseText;
console.log(result);
doSomethingElseWith(result);
// all uses of `result` must be inside this function
});
xhr.send();
...

Message Passing issues in Chrome Extension

I'm trying to pass a message from my content script to my background page. This error occurs when the content script is executed:
Uncaught TypeError: Cannot call method 'sendRequest' of undefined
Content Script:
function injectFunction(func, exec) {
var script = document.createElement("script");
script.textContent = "-" + func + (exec ? "()" : "");
document.body.appendChild(script);
}
function login() {
chrome.extension.sendMessage({greeting: "hello"}, function(response) {
console.log(response.farewell);
});
var d = window.mainFrame.document;
d.getElementsByName("email")[0].value = "I need the response data here";
d.getElementsByName("passwort")[0].value = "Here too.";
d.forms["login"].submit();
}
injectFunction(login, true);
Background:
chrome.extension.onMessage.addListener(
function(request, sender, sendResponse) {
if (request.greeting == "hello")
sendResponse({farewell: "goodbye"});
});
manifest.json:
{
"name": "Sephir Auto-Login",
"version": "1.0",
"manifest_version": 2,
"description": "Contact x#x.com for support or further information.",
"options_page": "options.html",
"icons":{
"128":"icon.png"
},
"background": {
"scripts": ["eventPage.js"]
},
"content_scripts": [
{
"matches": ["https://somewebsite/*"],
"js": ["login.js"]
},
{
"matches": ["somewebsite/*"],
"js": ["changePicture.js"]
}
],
"permissions": [
"storage",
"http://*/*",
"https://*/*",
"tabs"
]
}
Those are the examples on the documentation from google, so they should work.
Any help? I'm completely lost.
The problem is caused by your misunderstanding of the script executing environment. Read Chrome extension code vs Content scripts vs Injected scripts for more information. To be precise, you're using a form of this method to execute code in the context of a web page. Web pages do not have any access to the chrome.extension API.
I suggest to rewrite your code to not use injected scripts, because it's not necessary in this case.
function login() {
chrome.extension.sendRequest({greeting: "hello"}, function(response) {
console.log(response.farewell);
});
var d = document.getElementById('mainFrame').contentDocument;
d.getElementsByName("email")[0].value = "I need the response data here";
d.getElementsByName("passwort")[0].value = "Here too.";
d.forms["login"].submit();
}
login();
* Only works if the frame is located at the same origin. Otherwise, you need this method to execute code correctly.
sendRequest and onRequest are deprecated. You need to use sendMessage and onMessage.
Also, you are injecting function to the DOM, that makes it run outside the content script context so chrome.extension API is no longer available for this function.

Categories