Access user options in background.js in Chrome Extension - javascript

I'm trying to access chrome.storage.sync where I store some user options in my background.js but the asynchronous nature of chrome.storage.sync.get is causing me issues.
If I try and use chrome.storage.sync.get within my chrome.webRequest.onBeforeRequest.addListener the callback isn't quick enough for the function to use it.
I have tried adding the user options as a global variable within background.js but it appears to me that that value doesn't persist.
Anyone else using user options in background.js?
function getoption(){
chrome.storage.sync.get({
radarpref: 'nothing',
}, function(items) {
console.log(items.radarpref);
return items.radarpref;
});
}
var hold = getoption();
console.log (hold) //this returns hold value
chrome.webRequest.onBeforeRequest.addListener(
function(info) {
//this doesn't work - yet
console.log('i dont see the hold');
console.log(hold) //hold not returned when callback ran
...

If you need to synchronously use settings from any async storage - the best way to do it is to cache it.
You need to load the settings to the cache on background.js start and then you need to update cache each time chrome.storage.onChanged event triggered.
Example how to do it:
manifest.js
{
"manifest_version": 2,
"name": "Settings Online demo",
"description": "Settings Online demo",
"applications": {
"gecko": {
"id": "852a5a44289192c3cd3d71e06fdcdb43b1437971#j2me.ws"
}
},
"version": "0.0.1",
"background": {
"scripts": ["background.js"]
},
"permissions": [
"storage",
"webRequest",
"webRequestBlocking",
"<all_urls>"
],
"options_ui": {
"page":"properties.html",
"chrome_style": true
}
}
Note that you need to have non-temporary application id if you need to work it on firefox, <all_urls> permission is needed to get access to any url request processing.
background.js
((storage) => {
let settings = (function(properties) {
// Save settings
this.set = (properties,ok) => {
for(key in properties || {}){
this[key]=properties[key];
}
storage.set(
properties
,() => {
ok(settings);
});
};
//Default values processing
for(key in properties || {}){
this[key]=properties[key];
}
// Initial settings read
storage.get(properties,(properties) => {
for(key in properties){
this[key]=properties[key];
}
});
// Listen settings change and cache it
chrome.storage.onChanged.addListener((msg) => {
for(key in msg){
this[key]=msg[key].newValue;
}
});
return this;
}).call({},{"property":"default","name":"me"})
chrome.webRequest.onBeforeRequest.addListener(
function(info) {
// Update and persist settings
settings.set({"lastRequest":info},()=>{console.log("Settings saved")});
console.log('Catch', settings.name,settings.property);
},{urls:["https://*/*"]});
})(chrome.storage.sync || chrome.storage.local);
Note that I use chrome.storage.sync || chrome.storage.local because some browsers (Opera, mobile browsers) do not support sync-storage, but support local storage.
And the property page to see how can property changes are processing:
properties.html
<html>
<head>
<script src="properties.js" type="text/javascript"></script>
</head>
<body>
<label>Property:<input id="property" type="text"></label>
<input id="save-properties" value="save" type="submit">
</body>
</html>
properties.js
((storage) => {
let saveOptions = () => {
let property = document.getElementById("property").value;
storage.set({
"property": property
},() => {
window.close();
});
}
let restoreOptions = () => {
storage.get({
"property": "default"
}, (properties) => {
document.getElementById("property").value = properties.property;
});
document.getElementById("save-properties").addEventListener("click", saveOptions);
}
document.addEventListener("DOMContentLoaded", restoreOptions);
})(chrome.storage.sync || chrome.storage.local);
That's all :)
P.S> This solution has a weak point: if your app is settings-sensitive and can't work with default settings, or you need to be sure that you're using custom settings on start - you need to delay background.js start while settings are not loaded. You may to it with callback or with promise:
background.js - wait while settings will be loaded with callback
((storage) => {
let settings = (function(properties) {
// Update settings
this.set = (properties,ok) => {
for(key in properties || {}){
this[key]=properties[key];
}
storage.set(
properties
,() => {
ok(settings);
});
};
//Default values processing
for(key in properties || {}){
this[key]=properties[key];
}
// Listen settings change and cache it
chrome.storage.onChanged.addListener((msg) => {
for(key in msg){
this[key]=msg[key].newValue;
}
});
// Initial settings read
storage.get(properties,(properties) => {
for(key in properties){
this[key]=properties[key];
}
mainLoop();
});
return this;
}).call({},{"property":"default","name":"me"})
let mainLoop = () => {
//.. all you settings-sensitive code
chrome.webRequest.onBeforeRequest.addListener(
function(info) {
// Update settings and persist it
settings.set({"lastRequest":info},()=>{console.log("Settings saved")});
console.log('Catch', settings.name,settings.property);
},{urls:["https://*/*"]});
};
})(chrome.storage.sync || chrome.storage.local);
background.js - wait while settings will be loaded with promise
((storage) => {
let settings = ((properties) => {
this.set = (properties) => {
for(key in properties || {}){
this[key]=properties[key];
}
return new Promise((ok,err) => {
storage.set(
properties
,() => {
ok(settings);
});
});
};
return new Promise((ok,err) => {
//Default values processing
for(key in properties || {}){
this[key]=properties[key];
}
// Listen settings change and cache it
chrome.storage.onChanged.addListener((msg) => {
for(key in msg){
this[key]=msg[key].newValue;
}
});
// Initial settings read
storage.get(properties,(properties) => {
for(key in properties){
this[key]=properties[key];
}
ok(this);
});
});
}).call({},{"property":"default","name":"me"}).then((settings) => {
//.. all you settings-sensitive code
chrome.webRequest.onBeforeRequest.addListener(
function(info) {
// Update settings and persist it
settings.set({"lastRequest":info}).then(()=>{console.log("Settings saved")});
console.log('Catch', settings.name,settings.property);
},{urls:["https://*/*"]});
}).catch(()=>{});
})(chrome.storage.sync || chrome.storage.local);
Read more
Storage specs/firefox:
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage
Storage spect/chrome: https://developer.chrome.com/apps/storage
Permission requests/firefox:
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Request_the_right_permissions

Related

How to get current tab URL using Manifest v3?

How do I get the URL of the current tab in the background service worker in MV3?
Here's what I have:
let currentURL;
chrome.action.onClicked.addListener(handleBrowserActionClicked);
chrome.commands.onCommand.addListener(function(command) {
console.log("Command:", command);
handleBrowserActionClicked();
});
function handleBrowserActionClicked() {
togglePlugin();
}
function togglePlugin() {
console.log("toggle plugin");
chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, { greeting: "activateFeedback" });
});
}
// Fires when the active tab in a window changes.
chrome.tabs.onActivated.addListener(function () {
console.log("TAB CHANGED")
//firstTimeRunning = true
//feedbackActivated = false
currentURL = getTab()
.then(console.log("Current URL: " + currentURL))
})
// Fired when a tab is updated.
chrome.tabs.onUpdated.addListener(function () {
console.log("TAB UPDATED")
currentURL = getTab() // line 32
.then(console.log("Current URL: " + currentURL))
})
async function getTab() {
let queryOptions = { active: true, currentWindow: true };
let [tab] = await chrome.tabs.query(chrome.tabs[0].url); // line 38
return tab;
}
Right now the service worker is logging "Current URL: [object Promise]" instead of, for example, "https://www.google.com"
It is also giving an error in the console (see comments above for line numbers)
background.js:38 Uncaught (in promise) TypeError: Cannot read property 'url' of undefined
at getTab (background.js:38)
at background.js:32
I think it may be something to do with my limited knowledge of promises!
Please help.
Thank you in advance.
You function getTab seems not right, you are currently trying to query on the url. Not on the query options. The following function should work.
async function getTab() {
let queryOptions = { active: true, currentWindow: true };
let tabs = await chrome.tabs.query(queryOptions);
return tabs[0].url;
}
Also make sure you have the tabs permission.
In the listener you also don't use the correct async/promise method two examples using Promise.then and await.
Promise.then:
chrome.tabs.onUpdated.addListener(function () {
console.log("TAB UPDATED")
getTab().then(url => {
console.log(url);
})
})
await:
chrome.tabs.onUpdated.addListener(async function () {
console.log("TAB UPDATED")
let url = await getTab()
console.log(url)
})
For the "Error: Tabs cannot be queried right now (user may be dragging a tab)." error you can look at this answer, which suggest a small delay before querying the tab url.
const tab = (await chrome.tabs.query({ active: true }))[0]
Simply use "activeTab" permission in manifest.json
Add activeTab in your manifest.json.
"permissions": [
"activeTab",
],
And In Background.js
chrome.action.onClicked.addListener((tab) => {
console.log(tab.url);
});
I'm sure It will help you to get the current tab URL.
useful links - ActiveTab

devtools - send message to background page when all network requests are completed

I have this code in my chrome extension (I'm making it for learning propouse)
manifest.json
{
"manifest_version": 2,
"name": "test",
"description": "test",
"permissions": [
"webRequest",
"activeTab",
"downloads"
],
"minimum_chrome_version": "85.0",
"devtools_page": "index.html",
"background": {
"scripts": [
"js/background.js"
],
"persistent": true
},
"browser_action": {
},
"version": "1.0.0",
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
}
devtools.js
let button = document.getElementById('process-data');
let fileChunks = [];
chrome.devtools.panels.create('test', '', '/index.html');
let port = chrome.runtime.connect({
name: 'devtools'
});
// port.onMessage.addListener( (message) => {});
chrome.devtools.network.onRequestFinished.addListener( (request) => {
if(request.response.status === 206 ){
console.log(request);
request.getContent( (content, encoding) => {
let data = `data:audio/mpeg;base64,${content}`;
fetch(data)
.then( (res) => res.arrayBuffer() )
.then( (buffer) => {
console.log(buffer);
fileChunks = [...buffer];
});
});
}
});
button.addEventListener('click', () => {
port.postMessage({type: 'processBuffer', data: fileChunks});
});
What I want to do is to inspect network requests from a certain website and get the response body content. On this point all seems working fine, I've solved an issue related to the request.method that I don't know why is undefined and will cause that the extension wasn't working. Now I've implemented a simple messaging system with background page as you can see, I want to pass the retrived body content to the background page of the extension to process it. Since I don't know how to check if no request are made, I'm implementing a button to send the content to background for processing but I'm unable to access to the button that is in the panel
background.js
let chunks = [];
chrome.runtime.onConnect.addListener( (port) => {
let devtoolsListener = (message, sender, sendResponse) => {
if(message.type === 'processBuffer'){
processChunks(message.data);
}
}
port.onMessage.addListener(devtoolsListener);
port.onDisconnect.addListener( () => {
devToolsConnection.onMessage.removeListener(devToolsListener);
});
});
const processChunks = async (chunks) => {
console.log(chunks);
}
index.html
<script src="js/devtool.js"></script>
<button id="process-data">Process audio data</button>
These are the errors I get
Uncaught TypeError: Cannot read property 'addEventListener' of null
Uncaught (in promise) TypeError: buffer is not iterable
Uncaught TypeError: Cannot read property 'addEventListener' of null
Error in event handler: ReferenceError: devToolsConnection is not defined
Is there any way to detect when a page complete all the requests so a message can be sent to background script to start data processing?
I assume that your button just didn't create on the script run time.
change it to:
window.addEventListener('DOMContentLoaded', (event) => {
let button = document.getElementById('process-data');
button.addEventListener('click', () => {
port.postMessage({type: 'processBuffer', data: fileChunks});
});
});
And I believe you want:
port.onMessage.removeListener(devToolsListener);
not
devToolsConnection.onMessage.removeListener(devToolsListener);

Chrome extension: intercept HTTP Response

I saw many pages talking about how to intercept the HTTP Response from a site. I'm trying this: Chrome Extension - How to get HTTP Response Body?
There are no execuble programs... this is my code:
manifest.json:
{
"manifest_version": 2,
"name": "Extension Name",
"description": "Some Desc.",
"version": "1.1",
"browser_action": {
"default_icon": "icon.png",
"default_popup": "index.html"
},
"permissions": [
"activeTab",
"storage",
"tabs",
"https://*.google.com/"
],
"content_scripts": [
{
"matches": ["https://*.google.com/"],
"run_at": "document_start",
"js": ["contentscript.js", "inject.js"]
}
],
"web_accessible_resources": ["injected.js"]
}
index.html:
<html>
<head>
<script src="contentscript.js"></script>
</head>
<body>
<p>HTTP INTERCEPTOR</p>
</body>
</html>
injected.js:
(function(xhr) {
console.log('injeced file');
var XHR = XMLHttpRequest.prototype;
var open = XHR.open;
var send = XHR.send;
var setRequestHeader = XHR.setRequestHeader;
XHR.open = function(method, url) {
this._method = method;
this._url = url;
this._requestHeaders = {};
this._startTime = (new Date()).toISOString();
return open.apply(this, arguments);
};
XHR.setRequestHeader = function(header, value) {
this._requestHeaders[header] = value;
return setRequestHeader.apply(this, arguments);
};
XHR.send = function(postData) {
this.addEventListener('load', function() {
var endTime = (new Date()).toISOString();
var myUrl = this._url ? this._url.toLowerCase() : this._url;
if(myUrl) {
if (postData) {
if (typeof postData === 'string') {
try {
// here you get the REQUEST HEADERS, in JSON format, so you can also use JSON.parse
this._requestHeaders = postData;
} catch(err) {
console.log('Request Header JSON decode failed, transfer_encoding field could be base64');
console.log(err);
}
} else if (typeof postData === 'object' || typeof postData === 'array' || typeof postData === 'number' || typeof postData === 'boolean') {
// do something if you need
}
}
// here you get the RESPONSE HEADERS
var responseHeaders = this.getAllResponseHeaders();
if ( this.responseType != 'blob' && this.responseText) {
// responseText is string or null
try {
// here you get RESPONSE TEXT (BODY), in JSON format, so you can use JSON.parse
var arr = this.responseText;
// printing url, request headers, response headers, response body, to console
console.log(this._url);
console.log(JSON.parse(this._requestHeaders));
console.log(responseHeaders);
console.log(JSON.parse(arr));
} catch(err) {
console.log("Error in responseType try catch");
console.log(err);
}
}
}
});
return send.apply(this, arguments);
};
})(XMLHttpRequest);
inject.js I set a timeout so I can enable the debugger:
/**
* code in inject.js
* added "web_accessible_resources": ["injected.js"] to manifest.json
*/
setTimeout(function() {
var s = document.createElement('script');
s.src = chrome.extension.getURL('injected.js');
s.onload = function() {
this.remove();
console.log('remove');
};
(document.head || document.documentElement).appendChild(s);
}, 10000);
Why the code is not injected into https://www.google.com/? Inspecting the DOM I don't see the code... the code runs and xhr is started but the methods open, setRequestHeader and send are never called.
The code is from my answer here.
Content Script, in that case, is used to communicate with injected.js.
Sample code is as follows:
/**
* Content script currently only used to communicate extension state on off message to injected.js
* Sends back response to extension (popup.js) after sending message to injected.js
*/
$(function(){
// localStorage is different from chrome.storage
// localStorage for injected script, and chrome.storage for extension script (popup.js) and contentscript.js
chrome.storage.sync.get("state", function (data) {
if (typeof data.state === 'undefined') {
chrome.storage.sync.set({"state": "on"}, function() {}); // async
}
console.log("Content Script State: " + data.state);
});
// message from extension script to this content script.
// will be used to receive enable disable messages
// sends response in 'status' variable
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
console.log(sender.tab ?
"content script receiving message from a content script:" + sender.tab.url :
"content script receiving message from the extension");
if (request.toggle === true) {
chrome.storage.sync.set({"state": "on"}, function() { console.log("Content Script State Updated: on"); }); // async
var data = {
app_state: "on"
};
document.dispatchEvent(new CustomEvent("app_state_message", {detail: data}));
// cannot return state in function since above .set is async and popup.js does not receive the response
sendResponse({state: "on"});
} else if (request.toggle === false) {
chrome.storage.sync.set({"state": "off"}, function() { console.log("Content Script State Updated: off"); }); // async
var data = {
app_state: "off"
};
document.dispatchEvent(new CustomEvent("app_state_message", {detail: data}));
sendResponse({state: "off"});
} else {
sendResponse({state: "error"});
}
});
});
Please read more on Content Scripts. Hope you find this useful.

Add to home Screen propmpt not showing using manifest json

I need to add prompt for ADD TO HOME SCREEN using manifest.json but it is not showing while my pwa score in audit is 100%
I have dist folder like below:-
My Manifest json consisting below:-
{
"name": "xyz",
"short_name": "xyz",
"icons": [
{
"src": "/xyz/static/img/icons/xy-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/xyz/static/img/icons/xy-icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "xyz/index.html",
"scope": ".",
"display": "standalone",
"background_color": "#0628b9",
"theme_color": "#000000"
}
and i am using workbox plugin for service worker and i tried with normal service worker too like below :-
sw.js
var VERSION = '20';
self.addEventListener('install', function(e) {
e.waitUntil(caches.open(VERSION).then(cache => {
return cache.addAll([
'https://cfjedimaster.github.io/nomanssky/client/index.html'
]);
}))
});
self.addEventListener('fetch', function(e) {
var tryInCachesFirst = caches.open(VERSION).then(cache => {
return cache.match(e.request).then(response => {
if (!response) {
return handleNoCacheMatch(e);
}
// Update cache record in the background
fetchFromNetworkAndCache(e);
// Reply with stale data
return response
});
});
e.respondWith(tryInCachesFirst);
});
self.addEventListener('activate', function(e) {
e.waitUntil(caches.keys().then(keys => {
return Promise.all(keys.map(key => {
if (key !== VERSION)
return caches.delete(key);
}));
}));
});
function fetchFromNetworkAndCache(e) {
// DevTools opening will trigger these o-i-c requests, which this SW can't handle.
// There's probaly more going on here, but I'd rather just ignore this problem. :)
// https://github.com/paulirish/caltrainschedule.io/issues/49
if (e.request.cache === 'only-if-cached' && e.request.mode !== 'same-origin') return;
return fetch(e.request).then(res => {
// foreign requests may be res.type === 'opaque' and missing a url
if (!res.url) return res;
// regardless, we don't want to cache other origin's assets
if (new URL(res.url).origin !== location.origin) return res;
return caches.open(VERSION).then(cache => {
// TODO: figure out if the content is new and therefore the page needs a reload.
cache.put(e.request, res.clone());
return res;
});
}).catch(err => console.error(e.request.url, err));
}
function handleNoCacheMatch(e) {
return fetchFromNetworkAndCache(e);
}
and my pwa score in lighthouse is 100%.
but i am not able to see the prompt add to home screen.
When testing/debugging A2HS, most of the work is clearing out previous tests and installs.
Over and over again.
Browsers intentionally remember what was done last time so the user is not pestered with install prompts.
Totally clearing out the cache may be necessary if you want to see the prompt again.
If you said no to a previous prompt, it will not ask again for XX months.
If you already installed, it should not ask again.
A few things to look for
-- in Desktop Chrome
-- Remove icon from chrome://apps/
-- If that does not work, you may need to clear the cache

Remember state chrome extension

I use a chrome extension to fire two content scripts to inject css. If the user opens the page the contentscript-on.js loads (defined in my manifest.json):
manifest.json
{
"name": "tools",
"version": "1.1",
"description": "tools",
"browser_action": {
"default_icon": "icon-on.png",
"default_title": "tools"
},
"manifest_version": 2,
"content_scripts": [
{
"matches": [ "*://*/*" ],
"include_globs": [ "*://app.example.*/*" ],
"js": ["jquery-1.11.0.min.js", "contentscript-on.js"]
}
],
"background": {
"scripts": ["background.js"],
"persistent": true
},
"permissions": [
"storage",
"https://*.app.example.de/*", "tabs", "webNavigation"
]
}
background.js
function getToggle(callback) { // expects function(value){...}
chrome.storage.local.get('toggle', function(data){
if(data.toggle === undefined) {
callback(true); // default value
} else {
callback(data.toggle);
}
});
}
function setToggle(value, callback){ // expects function(){...}
chrome.storage.local.set({toggle : value}, function(){
if(chrome.runtime.lastError) {
throw Error(chrome.runtime.lastError);
} else {
callback();
}
});
}
chrome.browserAction.onClicked.addListener( function(tab) {
getToggle(function(toggle){
toggle = !toggle;
setToggle(toggle, function(){
if(toggle){
//change the icon after pushed the icon to On
chrome.browserAction.setIcon({path: "icon-on.png", tabId:tab.id});
//start the content script to hide dashboard
chrome.tabs.executeScript({file:"contentscript-on.js"});
}
else{
//change the icon after pushed the icon to Off
chrome.browserAction.setIcon({path: "icon-off.png", tabId:tab.id});
//start the content script to hide dashboard
chrome.tabs.executeScript({file:"contentscript-off.js"});
}
});
});
});
contentscript-on.js
$(document).ready(function() {
chrome.storage.local.get('toggle', function(data) {
if (data.toggle === false) {
return;
} else {
// do some css inject
}
});
});
contentscript-off.js
$(document).ready(function() {
// set css to original
});
Everything works fine, but how can I save the "state" of the icon? If the user close the browser and open it again, the last used contentscript should load.
Thank you very much for your help.
You have two methods (at least), one is "old" and one is "new".
Old: localStorage
Your extension pages share a common localStorage object you can read/write, and it is persistent through browser restarts.
Working with it is synchronous:
var toggle;
if(localStorage.toggle === undefined){
localStorage.toggle = true;
}
toggle = localStorage.toggle;
chrome.browserAction.onClicked.addListener( function(tab) {
var toggle = !toggle;
localStorage.toggle = toggle;
/* The rest of your code; at this point toggle is saved */
});
It's simple to work with, but there are downsides: localStorage context is different for content scripts, so they need to communicate via Messaging to get the values from the background script; also, complications arise if the extension is used in Incognito mode.
New: chrome.storage API
To work with the new method, you need permission "storage" in the manifest (does not generate a warning).
Also, unlike localStorage, working with it is asynchronous, i.e. you will need to use callbacks:
function getToggle(callback) { // expects function(value){...}
chrome.storage.local.get('toggle', function(data){
if(data.toggle === undefined) {
callback(true); // default value
} else {
callback(data.toggle);
}
});
}
function setToggle(value, callback){ // expects function(){...}
chrome.storage.local.set({toggle : value}, function(){
if(chrome.runtime.lastError) {
throw Error(chrome.runtime.lastError);
} else {
callback();
}
});
}
chrome.browserAction.onClicked.addListener( function(tab) {
getToggle(function(toggle){
toggle = !toggle;
setToggle(toggle, function(){
/* The rest of your code; at this point toggle is saved */
});
});
});
Asynchronous code is a bit harder to work with, but you get some advantages. Namely, content scripts can use chrome.storage directly instead of communicating with the parent, you can watch for changes with onChanged, and you can use chrome.storage.sync instead of (or together with) chrome.storage.local to propagate changes to all browsers a user is logged into.
EDIT
I'm including a full solution, since the OP made a mistake of mixing per-tab state and global state.
contentscript.js
$(document).ready(function() {
chrome.storage.local.get('toggle', function(data) {
if (data.toggle === false) {
return;
} else {
/* do some css inject */
}
});
chrome.storage.onChanged.addListener(function(changes, areaName){
if(areaName == "local" && changes.toggle) {
if(changes.toggle.newValue) {
/* do some css inject */
} else {
/* set css to original */
}
}
});
});
background.js:
/* getToggle, setToggle as above */
function setIcon(value){
var path = (value)?"icon-on.png":"icon-off.png";
chrome.browserAction.setIcon({path: path});
}
getToggle(setIcon); // Initial state
chrome.browserAction.onClicked.addListener( function(tab) {
getToggle(function(toggle){
setToggle(!toggle, function(){
setIcon(!toggle);
});
});
});
This way, you only need one content script.

Categories

Resources