Can I share code between different parts of Chrome Extension? - javascript

Let's say, I have a function:
var rand = function(n) {
return Math.floor(Math.random() * n);
}
Can I use this function in both Content Script and Background Script without copypaste?
Thank you.

Yes.
You could have an external JS file which is loaded as part of the background and the content script (like any normal JS file). Just add it to the background and content script file arrays in the manifest and it will be loaded for you.
For example if our shared function reside in sharedFunctions.js, the content script using them is in mainContentScript.js and the background code in mainBackground.js (all in the js subfolder) we can do something like this in the manifest:
"background": {
"scripts": [ "js/sharedFunctions.js", "js/mainBackground.js" ]
},
"content_scripts": [
{
"matches": ["*://*/*"],
"js": ["js/sharedFunctions.js", "js/mainContentScript.js"]
}
]
Note: Make sure to load it in the right order, before other files using it.
Or you can also add it as a script tag (with a relative URL) in the background.html and in the manifest only add it to the content script's JS array. So the manifest will look like:
"background": {
"page": "background.html"
},
"content_scripts": [
{
"matches": ["*://*/*"],
"js": ["js/sharedFunctions.js", "js/mainContentScript.js"]
}
]
and the background.html file will have this script tag:
<script type="text/javascript" src="js/sharedFunctions.js"></script>
Edit: Also, For sharing with other scopes in other parts of the extension (unfortunately not available in the content script).
You can also have the functions you want to share reside in the background script and reach them via chrome.runtime.getBackgroundPage() which you can read more about here:
https://developer.chrome.com/extensions/runtime#method-getBackgroundPage
So, let's say you have your rand() function declared as a global variable in the background script (which means it's a property on the background's window object), you can do something of this sort at the beginning the script in the other scope (this can be a popup context like a browserAction window):
var background, newRandomNumber;
chrome.runtime.getBackgroundPage(function(backgroundWindow){
background = backgroundWindow;
newRandomNumber = background.rand();
})
This way you can also use the variable background to access any property or method set on the background's window object. Be mindful, that this function runs asynchrounusly, meaning that only after the callback is called will the variables background and newRandomNumber will be defined.

Yes, you can, by putting it in a separate JS file and loading it in both.
Say, you have a file utils.js that contain all such functions.
Then you can load the background page like this:
"background": {
"scripts": [ "utils.js", "background.js" ]
},
And the content script like this:
"content_scripts": [
{
"matches": ["..."],
"js": ["utils.js", "content.js"]
}
],
Or, if you're using programmatic injection, chain the calls like this:
chrome.tabs.executeScript(tabId, {file: "utils.js"}, function(){
chrome.tabs.executeScript(tabId, {file: "content.js"}, yourActualCallback);
});

Related

Is message passing between multiple content scripts possible without using the background script?

When migrating a Chrome Extension to Manifest v3 we are getting rid of the background script and are instead using service workers.
The problem is that we previously sent messages from multiple content scripts to another content script through the background script, and this is no longer possible because in Manifest v3 the background script will become inactive after a while.
Is it possible to send messages between multiple content scripts without using the background script?
This is an example of how the content scripts are setup, sender.js is available in multiple iframes while receiver.js only is present in the top document.
"content_scripts": [
{
"js": ["receiver.js"],
"all_frames": false,
"matches": ["<all_urls>"]
},
{
"js": ["sender.js"],
"all_frames": true,
"matches": ["<all_urls>"],
"run_at": "document_start"
}
]
You can send messages from contents scripts to other content scripts with chrome.storage.local, no service worker required.
Proof of concept:
The counter has two purposes:
Allows content scripts to distinguish their own messages from other content scripts' messages.
Guarantees that chrome.storage.onChanged always fires, even if the data hasn't changed.
You need to execute the entire code inside the async function every time you send a message.
You could also use random numbers instead of a counter.
Advantage: You don't have to read the old counter value from storage.
Disadvantage: You need to make sure there are no collisions.
I don't know if the "event doesn't wake up the service worker" bug can occur in content scripts.
Instead of chrome.storage.local, you could use chrome.storage.session, but you'd need to set the access level: Storage areas > storage.session
manifest.json
{
"manifest_version": 3,
"name": "Content Script Messaging with Storage",
"version": "1.0",
"action": {
},
"content_scripts": [
{
"matches": ["*://*/*"],
"js": ["content_script.js"]
}
],
"permissions": [
"storage"
]
}
content_script.js
let counter = -1;
function storage_on_changed(changes, areaName) {
if (changes?.message?.newValue?.counter == counter) {
console.log("The message came from this content script", changes);
}
else {
console.log("The message came from another content script", changes);
}
}
chrome.storage.onChanged.addListener(storage_on_changed);
(async () => {
let { message } = await chrome.storage.local.get("message");
if (message === undefined) {
counter = 0;
}
else {
counter = message.counter + 1;
}
await chrome.storage.local.set({ message: {counter, data: document.URL} });
})();

XSS "Blocked a frame with origin from accessing a cross-origin frame" error in content script for a Chrome extension

I've had this extension in the Google Chrome store for a while now. After doing a maintenance update I noticed that the following line from the content.js (content script):
//Get top document URL (that is the same for all IFRAMEs)
var strTopURL = window.top.document.URL;
is now throwing the following exception when the loaded page has an IFRAME in it:
Blocked a frame with origin "https://www.youtube.com" from accessing a
cross-origin frame.
Like I said, it used to be the way to obtain the top document URL for your extension (from the content script). So what's the accepted way to do it now?
PS. Again, I'm talking about a Google Chrome extension (and not just a regular JS on the page.)
EDIT: This script is running under the content_scripts in the manifest.json that is defined as such:
"content_scripts": [
{
"run_at": "document_end",
"all_frames" : true,
"match_about_blank": true,
"matches": ["http://*/*", "https://*/*"],
"js": ["content.js"]
}
],
The content script should ask your background script to do it via messaging:
chrome.runtime.sendMessage('getTopUrl', url => {
// use the URL here inside the callback or store in a global variable
// to use in another event callback that will be triggered in the future
console.log(url);
});
// can't use it right here - because the callback runs asynchronously
The background script should be declared in manifest.json:
"background": {
"scripts": ["background.js"],
"persistent": false
},
You'll also need need specific URL permissions in manifest.json or allow all URLs:
"permissions": ["<all_urls>"]
And the listener in the background script:
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg === 'getTopUrl') {
chrome.tabs.get(sender.tab.id, tab => sendResponse(tab.url));
// keep the message channel open for the asynchronous callback above
return true;
}
});

Can I use an external array to match URLs for content script injection?

I'm working on a chrome extension where I'd like to inject a content script into a list of urls. Usually I'd use the regular syntax:
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["http://www.google.com/*"],
"css": ["mystyles.css"],
"js": ["jquery.js", "myscript.js"]
}
],
...
}
But for the match patterns I'd like to pull the array from a server. Is there a way to programmatically set the "matches" array (from the background.js file for example)?
As far as I know, you cannot modify your manifest.json file from within the extension. What you can do is programmatically inject your content scripts from the background page when the tab's URL matches one of the URLs you've got from the server.
Note that you will need tabs and <all_urls> permissions.
background.js
var list_of_URLs; //you populate this array using AJAX, for instance.
populate_list_of_URLs();
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab){
if (list_of_URLs.indexOf(tab.url) != -1){
chrome.tabs.executeScript(tabId,{file:"jquery.js"},function(){
chrome.tabs.executeScript(tabId,{file:"myscript.js"});
});
}
});

Pass parameter in javascript function inside chrome.tabs.executeScript

I'm bulilding a chrome extension.
In this part of code I try to find paragraphs in loaded page, for each paragraph in divs with id load and detail, I search if value of findCheckedFact is inside that paragraph if yes I cut I want to put that text inside a span with a class red.
findCheckedFact is a string. Here in +'console.log("Print:", str_p, " Fact:", findCheckedFact);' the text inside paragraph is defined but the parameter findCheckedFact is not defined, so I cant pass it?
This function I tried calling findTextInsideParagraphs("test");.
function findTextInsideParagraphs(findCheckedFact){
chrome.tabs.executeScript({file: "js/jquery-1.12.0.min.js"}, function() {
chrome.tabs.executeScript({"code":
'(function(findCheckedFact){'
+'$("#lead, #detail").find("p").each(function() {'
+'var str_p = $(this).text();'
+'console.log("Print:", str_p, " Fact:", findCheckedFact);'
+'if (str_p.indexOf( findCheckedFact) >= 0) {'
+'console.log("Yes kest");'
+'$(this).html($(this).html().replace(findCheckedFact, "<span class=\'red\'> $& </span>"));'
+'}'
+'});'
+'}(' + JSON.stringify("Fact: " , findCheckedFact) + '));'
});
});
}
This function I tried calling findTextInsideParagraphs("test");
Inside manifest.json I did add everythin possible to make it work:
"content_scripts": [
{
"matches": [
"<all_urls>",
"http://*/*",
"https://*/*"
],
"css": [
"custom-css.css"
],
"js": [
"js/jquery-1.12.0.min.js"
],
"run_at": "document_start"
}],
"background": {
"persistent": false,
"scripts": [
"js/jquery-1.12.0.min.js",
"background.js",
"popup.js",
"blic-fact-checker.js"
],
"css":["custom-css.css"]
},
"permissions": [
"background",
"notifications",
"contextMenus",
"storage",
"tabs",
"activeTab",
"http://localhost:5000/*",
"chrome-extension://genmdmadineagnhncmefadakpchajbkj/blic-fact-checker.js",
"chrome-devtools://devtools/bundled/inspector.html?&remoteBase=https://chrome-devtools-frontend.appspot.com/serve_file/#202161/&dockSide=undocked",
"http://*/*",
"https://*/*",
"<all_urls>"
],
Can somebody help me with this,really I can't find what's going on ?
First, read this explanation of different kinds of javascript code in Chrome extensions
As you see, your code in executeScript is a content script, and it has no direct access to the code in background and other proper extension pages. You have 3 main options:
1) send your FindCheckedFact to the content script with sendMessage or port.postMessage. Of course, in your content script you have to establish listeners for them, and put the actual code in these listeners.
2) save your variable in local or sync storage, putting your executeScript in the callback, and read them from your content script. If you need to re-read it sometimes, set a listener for storage change in the content script.
These solutions work both with a content script in a .js file, and an inline content script. But, since you choose inlining, and you have only to send a value one way, you can
3) just stringify your FindCheckedFact (with JSON.stringify or toString), and insert its value with +:
chrome.tabs.executeScript({"code":
'(function('+strFindCheckedFact+'){'
etc.

Use extension's localStorage wrapper function in an injected script

My extension's content script injects a script into gmail page via <script> element (main.js). The injected script needs some data from the settings stored in the extension's localStorage by options.js script of the options page.
The options page script can successfully use loadDomain() function that reads localStorage.domain value. This function is defined in a common functions script storage.js that is also injected on gmail page via <script> element along with main.js.
The problem is that loadDomain() returns undefined when called in the injected main.js instead of the actual values stored on the options page.
manifest.json:
"permissions": [
"tabs", "https://mail.google.com/*", "http://*/*, https://*/*"
],
"background": {
"scripts": ["js/background.js"],
"persistent": false
},
"browser_action": {
"default_icon": {
"38": "icon.png"
},
"default_title": "SalesUp",
"default_popup": "index.html"
},
"content_scripts": [
{
"matches": ["https://mail.google.com/*"],
"js": ["content.js"]
}
],
"web_accessible_resources": [
"js/jquery-1.10.2.min.js",
"js/gmail.js",
"main.js"
]
}
The chat discussion showed that loadDomain() was invoked from the <script>-injected main.js.
One part of the problem was caused by the fact that Chrome isolates the web page (with its scripts, also the injected ones) from the content scripts, as well as the background page. Another part was that localStorage is different on each domain (actually, origin), so whatever was stored inside the options page of the extension was not available in a content script that runs in the context of the web page and has access to its localStorage only, not the extension's localStorage.
The solution comprises two things:
instead of localStorage use chrome.storage.sync to store the extension settings or chrome.storage.local to store the temporary stuff that shouldn't be synced to Google servers.
use custom DOM-events to communicate between the injected script and the content script.
The code:
Injected main.js:
sending a request (detail key may be used to pass some data):
document.dispatchEvent(new CustomEvent("getDomains", {detail: {something: "hello"}}));
listening for a response:
document.addEventListener("receiveDomains", function(e) {
var domains = e.detail;
console.log("Received domains", domains);
...............
});
Content script content.js:
document.addEventListener("getDomains", function(e) {
chrome.storage.sync.get("domains", function(result) {
document.dispatchEvent(new CustomEvent("receiveDomains", {detail: result.domains}));
});
});
Options page options.js:
function save_options() {
chrome.storage.sync.set({domains: ["domain1", "domain2"]});
}

Categories