Injected HTML accessing the Chrome API and global variables - javascript

I'm working on a Chrome Extension, and I'm new to the process. The extension I'm working on injects an HTML sidebar into the page, injects java-script functions to the header, then let's the user press the buttons on the sidebar to create/save my extension's data.
However, when I want to save the information, I use localStorage, however, the localStorage always saves with respect to the current website. How can I use localStorage to save with respect to our extension?
Furthermore, I would like to use some global javascript variables in the chrome-extension. Where do these belong? I know I can't currently access them from the injected Javascript.
I've looked into message passing, and I've had some trouble with it. I'm not sure how it works in the scope of injected javascript into a page's header. I've tried working with this example, but my extension doesn't seem to catch the message.
// This function is called in the injected header javascript.
function sendToExtension() {
setTimeout(function() {
console.log('page javascript sending message');
window.postMessage({ type: 'page_js_type',
text: "Hello from the page's javascript!"},
'*' /* targetOrigin: any */);
}, 10);
}
// This is installed in a background script.
window.addEventListener('message', function(event) {
console.log('content_script.js got message:', event);
});

You have to use Chrome's sendMessage function and onMessage listener. See below:
function sendToExtension() {
console.log('Sending message');
chrome.runtime.sendMessage({ext: "myExtension"}, function(response) {
console.log(response.ack);
});
}
// Listener - Put this in the background script to listen to all the events.
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if (request.ext) {
console.log('Message received from ' + request.ext);
sendResponse({ack:'received'}); // This send a response message to the requestor
}
});

Related

Using postMessage to extension background page

I'm trying to send a CryptoKey (generated by SubtleCrypto.generateKey()) object from a contentscript to the background page of a webextension.
When using chrome.runtime.sendMessage to send the object, it is lost, as CryptoKey is not stringifyable (see also this question). Using window.postMessage to transfer the key to another window does work, as this method uses structured cloning..
Is there something similar to postMessage to send data that is not stringifyable to the background page of a webextension?
Thanks to the comment by #wOxxOm I solved it by creating a web accessible resource with this code:
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event) {
chrome.extension.getBackgroundPage().postMessage(event.data, "*");
}
This is triggered by the contentscript like that:
let iframe = document.createElement('iframe');
iframe.setAttribute('src', chrome.extension.getURL('webaccessible/index.html'));
iframe.addEventListener("load", () => {
iframe.contentWindow.postMessage(data);
})
While data is an object that contains the CryptoKey.
This data is received in the background script just like you normally would receive such messages:
window.addEventListener('message',(event) => {
console.log(event);
});

Porting Chrome extension to Edge

I have created a chrome extension to enable clipboard data access. The solution is explained in details here Implementing 'Paste' in custom context menu. Now the problem is how to port this extension to Edge. There is a tool for that I know I used it, and maybe it is working, but my problem is how to "consume" this extension, what is equivalent to chrome.runtime.sendMessage in Edge? In Chrome I used this https://developer.chrome.com/apps/messaging#external-webpage - the part 'Sending messages from webpages', but in Edge I just can't find anything similar. Thanks for your time and help.
There is runtime.sendMessage() in Edge too.
https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/sendMessage
The thing to keep in mind is that the runtime object is defined on the browser object, not chrome.
Sends a single message to event listeners within your extension or a different extension.
If sending to your extension, omit the extensionId argument. The runtime.onMessage event will be fired in each page in your extension, except for the frame that called runtime.sendMessage.
If sending to a different extension, include the extensionId argument set to the other extension's ID. runtime.onMessageExternal will be fired in the other extension.
Extensions cannot send messages to content scripts using this method. To send messages to content scripts, use tabs.sendMessage.
This is an asynchronous function that returns a Promise.
I managed to solve this. There is no way (at least I couldn't find it) to communicate from web page with extension background script (and only the background script can get data from the clipboard and has the 'browser' object defined). So what I did, I communicated with content script and content script communicated with background script. Here is the code.
PAGE CODE:
contextMenuPaste: function () {
if (getBrowserName() == 'EDGE') {
window.postMessage({
direction: "from-page-script"
}, "*");
}
},
window.addEventListener("message", function (event) {
if (event.source == window &&
event.data.direction &&
event.data.direction == "from-content-script") {
console.log('Data in page script', event.data.message);
}
});
CONTENT SCRIPT CODE
window.addEventListener("message", (event) => {
// If message came from page-script send request to background script to get clipboard data
if (event.source == window &&
event.data &&
event.data.direction == "from-page-script") {
browser.runtime.sendMessage({
message: "getClipboardData"
},
function(clipboardData) {
messagePageScript(clipboardData);
}
);
}
});
// Send clipboard data to page script
function messagePageScript(clipboardData) {
window.postMessage({
direction: "from-content-script",
message: clipboardData
}, "*");
}
BACKGROUND SCRIPT CODE
browser.runtime.onMessage.addListener(
function(req, sender, callback) {
if (req) {
if (req.message) {
if (req.message == "installed") {
console.log('Checking is extension is installed!');
callback(true);
}
else if(req.message = "getClipboardData") {
console.log('Get clipboard data');
callback(getDataFromClipboard());
}
}
}
return true;
}
);
function getDataFromClipboard() {
var bg = browser.extension.getBackgroundPage();
var helperTextArea = bg.document.getElementById('sandbox');
if (helperTextArea == null) {
helperTextArea = bg.document.createElement("textarea");
document.body.appendChild(helperTextArea);
}
helperTextArea.value = '';
helperTextArea.select();
// Clipboard data
var clipboardData = '';
bg.document.execCommand("Paste");
clipboardData = helperTextArea.value;
helperTextArea.value = '';
return clipboardData;
}
But there is one tiny issue. This code works if I have a break-point set on line
bg.document.execCommand("Paste");
and it doesn't if I don't have that break-point. I thought it is a trimming issue, added pauses, delayed executions but nothing helped. I will start a new question for that issues and will copy solution here (if I find one).

How to update the DOM using chrome.runtime.onMessageExternal function callback

I'm working on a Chrome app that received an address from an extension and is supposed to open that URL in the app window, using the webview tag and Chrome runtime API message sending.
I'm trying to get the chrome.window.create callback function to update the index.html page the was created.
It's not working as I planned though.
Here is the code:
chrome.runtime.onMessageExternal.addListener(
function (request, sender, sendResponse) {
chrome.app.window.create(
'index.html',
{PARAMETERS},
function () {
//get
var thisWindow = document.querySelector("webview");
thisWindow.setAttribute("src", request.url);
}
);
}
The index.html file is just a webview tag and some styling.
This opens an empty window once the message is received. However, it opens the page when I send again while the app is open, meaning that the callback probably tried to access the index.html file before it was created?
Thanks for reading!
The window.create callback function is called prior to the execution of the created windows onload event (see here). So presumably the DOM is not yet available at this stage. What you can do is, bind your modifications to the created windows onload event, thus ensuring the DOM is available.
chrome.app.window.create(
'index.html',
{
PARAMETERS
},
function (createdWindow) {
var contentWindow = createdWindow.contentWindow;
contentWindow.onload = function() {
var thisWindow = contentWindow.document.querySelector("webview");
thisWindow.setAttribute("src", request.url);
}
}
);

Passing message from one listener to another

I'm developing an extension for Chrome, and here's the workflow I'm trying to achieve:
popup sends message -> content script 1 listens -> content script 1 sends message -> content script 2 listens -> content script 2 performs action
In concept it's fine and dandy; what I've done is set up 2 listeners: one in each content script:
Popup:
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
console.log('popup send request');
chrome.tabs.sendMessage(tabs[0].id, obj);
});
Content script 1:
chrome.runtime.onMessage.addListener((function (request, sender) {
this.log('wg got request', 'request', request, 'sender', sender);
if (request.action == 'options-updated') {
this.updateOptions(request, (function() {
var obj = {action: 'refresh', WG: window.WG};
this.log('wg forwarded request');
chrome.runtime.sendMessage(obj); // attempting to forward another request
return true;
}).bind(this));
}
return true;
}).bind(window.WG));
Content script 2:
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
console.log('content script got request', 'request', request, 'sender', sender);
if (request.WG) {
request.WG.log('message', request.action);
if (request.action == 'refresh') {
WGRefresh(request.WG, request.options);
}
}
return true;
});
Problem is, content script 2 only receives the first message. So the output I'm getting is:
popup send request
content script got request (first, to ignore)
wg got request (same, first, don't ignore here)
wg forward request
And then nothing. The content script should have fired it again, and in the request I always send "action", which I check for in the listener, but for the logs I don't differentiate (it should ALWAYS log, so this means the request never gets there).
I've tried returning true in all the listeners, according to the documentation it will keep the chain running and not stop after the first hit, but even so it's not working. What am I doing wrong?!
There are 2 sendMessage functions in Chrome API.
chrome.runtime.sendMessage sends a message to all open extension pages (i.e. background, popup, etc.)
chrome.tabs.sendMessage sends a message to all content scripts from the extension in a given tab
So the call to chrome.runtime.sendMessage() in your first content script can't reach any other content script.
What's more, you can't call chrome.tabs directly from a content script.
To do what you want, you need to set up a background script that will act like a proxy between CS1 and CS2. Technically, you could use the popup, but it's unreliable, as the popup may be closed and then nobody would be listening. The background page (or better yet, an event page) is designed specifically for that purpose.
So the scheme becomes: popup -(tabs.sendMessage)-> CS1 -(runtime.sendMessage)-> background -(tabs.sendMessage)-> CS2
Do note that background page will need to know the tab ID to send the message to. If it's the same tab for some reason, e.g. you're trying to message across frames, you can use the sender parameter in the callback.
See Messaging docs for more details.

How do I provide an extra function to javascript code through an extension?

I want to write an extension that does the following:
Defines a custom function
Allows Javascript code loaded from the Internet to run such a function
The function should take as a parameter an event listener. Basically, something like:
newApiFunctionDefinedInExtension( function( responseHeaders ){
console.log("Headers arrived!", responseHeaders );
} ;
Then using chrome.webRequest, my extension (which made newApiFunctionDefinedInExtension available in the first place) will call the listener (in the locally loaded page) every time response headers are received from the network.
I am new to Chrome extensions and cannot find a way to make that happen. It would be great to know:
How to make a function defined in a module available to the loaded page's scope
How to make such an EventEmitter -- is there a constructor class I can extend?
My goal is simple: the loaded page should define a function, and that function should be called every time there is a network connection.
Every webRequest event receives information about a request, including the ID of the originating tab.
So, assuming that the tab exists note 1, you can use the following flow:
// background.js
chrome.webRequest.onHeadersReceived.addListener(function(details) {
if (details.tabId == -1)
return; // Not related to any tab
chrome.tabs.sendMessage(details.tabId, {
responseHeaders: details.responseHeaders
});
}, {
urls: ['*://*/*'], // e.g. all http(s) URLs. See match patterns docs
// types: ['image'] // for example, defaults to **all** request types
}, ['responseHeaders']);
Then, in a content script (declared in the manifest file), you take the message and pass it to the web page:
// contentscript.js
chrome.runtime.onMessage.addListener(function(message) {
// Assuming that all messages from the background are meant for the page:
document.dispatchEvent(new CustomEvent('my-extension-event', {
detail: message
}));
});
After doing that, your web page can just receive these events as follows:
document.addEventListener('my-extension-event', function(event) {
var message = event.detail;
if (message.responseHeaders) {
// Do something with response headers
}
});
If you want to put an abstraction on top (e.g. implementing a custom EventEmitter), then you need to inject a script in the main execution environment, and declare your custom API over there.
note 1. For simplicity, I assumed that the tab existed. In reality, that is never true for type "main_frame" (and "sub_frame"), because the page has not yet been rendered. If you want to get response headers for the top-level/frame documents, then you need to temporarily store the response headers in some data structure (e.g. a queue / dictionary) in the background page, and send the data to the content script whenever the script is ready.
This can be implemented by using chrome.runtime.sendMessage in the content script to send a message to the background page. Then, whenever a page has loaded and the content script is ready, the background page can use sendResponse to deliver any queued messages.

Categories

Resources