Chrome Extension embedding script in active web page, in MV3? - google-chrome

beautiful people on the internet. I am new to chrome extension not new to writing code though. I have implemented webpack to use external packages. One major one in my application is npm package by the name "mark.js".
My application works like this i want some specific words to be highlighted in active webpage using this package. I have written code for this to achieve the functionality but the problem is with loading the script in a web page. I have performed different aspect of loading script but that doesnot work. The new MV3 version have some strict rules.
I want to achieve anything similar of loading script in an active webpage. Please help.
btn.addEventListener("click", async () => {
console.log("BUTTON IS PRESSED!!");
try {
await chrome.tabs.query(
{ active: true, currentWindow: true },
async function (tabs) {
chrome.scripting.executeScript({
target: { tabId: tabs[0].id },
func: highlightText,
args: [arr],
});
}
);
} catch (e) {
console.log("ERROR AT CHROME TAB QUERY : ", e);
}
});
async function highlightText(arr) {
console.log(typeof Mark);
try {
var instance2 = new Mark(document.querySelector("body"));
// instance2.mark("is");
var success = [];
// const instance2 = new Mark(document.querySelector("body"));
await Promise.all(
arr.map(async function (obj) {
console.log("OBJECT TEXT : ", obj.text);
instance2.mark(obj.text, {
element: "span",
each: function (ele) {
console.log("STYLING : ");
ele.setAttribute("style", `background-color: ${obj.color};`);
if (obj.title && obj.title != "") {
ele.setAttribute("title", obj.title);
}
ele.innerHTML = obj.text;
success.push({
status: "Success",
text: obj.text,
});
},
});
})
);
console.log("SUCCESS : ", success);
} catch (error) {
console.log(error);
}
}

There's no need to use chrome.runtime.getURL. Since you use executeScript to run your code all you need is to inject mark.js before injecting the function.
Also, don't load popup.js in content_scripts, it's not a content script (these run in web pages), it's a script for your extension page. Actually, you don't need content_scripts at all.
btn.addEventListener('click', async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
const target = { tabId: tab.id };
const exec = v => (await chrome.scripting.executeScript({ target, ...v }))[0].result;
if (!await exec({ func: () => !!window.Mark })) {
await exec({files: ['mark.js.min'] });
await exec({func: highlightText, args: [arr] });
}
});

For V3 I assume you will want to use Content Scripts in your manifest to inject the javascript into every webpage it matches. I recently open-sourced TorpedoRead and had to do both V2 and V3, I recommend checking the repo as it sounds like I did something similar to you (Firefox is V2, Chrome is V3).
The code below need to be added to your manifest.json and this will execute on every page based on the matches property. You can read more about content scripts here: https://developer.chrome.com/docs/extensions/mv3/content_scripts/
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["yourscript.js"]
}
],

Related

Can't inject JavaScript into new tab from popup.js

I'M creating an extension that autofill's forms on different websites. There is a portion of the script that records the actions of the user with event listeners. After the recording is done the user click the test button that will launch a new tab and autofill's the form. I know launching a new tab for testing is not necessary but this function will be used later for another feature.
This is what I got so far. I had started over several times. I tried sending a message to content script. The chrome.windows,create function only launched from the popup script. I started chrome development a week ago. I'm totally confused.
Popup.js
`(async () =\> { const tab = await chrome.tabs.create({url: 'https://www.listyourself.net/ListYourself/listing.jsp'});
const tabId = tab.id; if (!tab.url)
await onTabUrlUpdated(tabId);
await chrome.scripting.executeScript({
target: {tabId},
files: \['test.js'\],
});`
})();`
I think your extension has two problems.
onTabUrlUpdated is not defined.
Execution of chrome.tabs.create causes the popup to lose focus, so the JavaScript is also terminated.
Here is a sample that fixes it.
manifest.json
{
"manifest_version": 3,
"name": "hoge",
"version": "1.0",
"permissions": [
"scripting"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_title": "hoge"
}
}
background.js
chrome.action.onClicked.addListener(() => {
main();
});
const test = () => {
console.log("test");
}
const main = async () => {
const tab = await chrome.tabs.create({ url: "https://nory-soft.web.app/" });
const tabId = tab.id;
if (!tab.url) {
chrome.tabs.onUpdated.addListener((updateTabId, changeInfo) => {
if ((updateTabId == tabId) && (changeInfo.status == "complete")) {
chrome.scripting.executeScript({
target: { tabId },
func: test
});
}
});
}
}

How can I focus a document programmatically from a Chrome extension (to paste content)

What I try to do is simple:
I am making a chrome extension.
This extension has a shortcut (e.g. alt + o).
When I press this shortcut it switches to a tab,
and should paste clipboard content in the page.
Here's the background script that reacts to the shortcut and inject code in page to simulate paste event:
// --- background.js
chrome.commands.onCommand.addListener((command) => {
switch (command) {
case 'switch_tab_and_paste_content':
switchToTabAndPasteContent()
break;
}
})
async function switchToTabAndPasteContent() {
const tab = (await chrome.tabs.query({})).filter((tab) => tab.title.toLowerCase().includes('my page'))[0]
// focus the window
await chrome.windows.update(tab.windowId, { focused: true })
// and focus the tab
await chrome.tabs.update(tab.id, { active: true })
/* inject paste emulation script */
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: function () {
pasteContent()
}
})
}
pasteContent() in the code above is a function contained in a content script. It's used to simulate paste event (just like a user would use ctrl + v on some page.)
// --- paste.js
async function pasteContent() {
try {
const wizElement = document.querySelector('c-wiz')
const clipboardItems = await navigator.clipboard.read()
const firstItem = clipboardItems[0]
const blob = await firstItem.getType('image/png')
const file = new File([blob], 'image.png', { type: 'image/png' })
const dataTransfer = new DataTransfer()
dataTransfer.items.add(file)
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
clipboardData: dataTransfer,
})
wizElement.dispatchEvent(pasteEvent)
}
catch (e) { console.log(e) }
}
and here's the simplified manifest.json
{
"name": "app-connector",
"version": 3,
"permissions": [ "tabs", "scripting", "activeTab" ],
"background": {
"service_worker": "background.js"
},
"content_scripts": [{
"matches": [ ... ],
"js": "paste.js"
}],
"commands": {
"switch_tab_and_paste_content": {
"description": "Switch tab"
}
}
}
However when I press the shortcut on my keyboard, the tab is focused but I get this exception
DOMException: Document is not focused.
I know the reason of that exception (it's because when I leave the tab the document loses focus, chrome.tabs.update(tab.id, { active: true }) activates the tab but it's not enough to focus back on the document)
What I want to know is if there is a way to bypass that (a chrome flag, a command-line argument, etc...).
Of course the ideal would be a native solution in the extension ecosystem but I didn't find/can't think of any solution. I tried opening/closing a popup fast to give focus back to the page but I still get this exception (it seems like it won't work unless I click in the page but it defeats the purpose of making an automated routine behind).
am I missing a permission?

While using chrome.debugger api it closes automatically and says target closed but the tab is still open, Can anyone help please?

Here is the relevant part of the code.
I am trying to capture network calls using chrome.debugger API here. Everything is ok but once the page loads or I click a link then after the first action it closes automatically
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.play) {
if (message.captureNtwrk) {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const tab = tabs[0];
tabId = tab.id;
chrome.debugger.attach({ tabId: tabId }, version, onAttach.bind(null, tabId));
});
}
if (message.captureScreen) {
}
if (message.captureEvent) {
}
}
if (message.stop) {
chrome.debugger.detach({ tabId: tabId });
}
});
// Function to run while debugging
const onAttach = (tabId) => {
if (chrome.runtime.lastError) console.log(chrome.runtime.lastError.message);
chrome.debugger.sendCommand({ tabId: tabId }, 'Network.enable');
chrome.debugger.onEvent.addListener(onEvent);
chrome.debugger.onDetach.addListener((source, reason) => {
console.log('Detached: ' + reason);
});
});
You can try disabling other extensions, especially 3rd party extensions added by desktop applications. One particular extension observed, which causes debugger to detach after page navigation is Adobe Acrobat.

Inject .html file from packed chrome extension directory into new window tab

I am building a Chrome extension (v3) and I am trying to load an index.html from the extension's directory, and replace the old html with my html in a newly created window. But instead of html, it gets rendered as text. This is what I got so far:
I stored the html file in my background.js :
let template = chrome.runtime.getURL("assets/index.html");
chrome.runtime.onInstalled.addListener( () => {
chrome.storage.sync.set({ "data": {"template" : template } });
});
In my popup.js, I first created a new window. Then I tried to inject a content script that overwrites the current html with my index.html file, as seen below:
button.addEventListener("click", async () => {
//get url of current tab
let tab = await chrome.tabs.query({ active: true, currentWindow: true });
//create new window
let newWindow = await chrome.windows.create({
url : tab[0].url,
type : "popup",
width: dims.cssWidth,
height: dims.cssHeight,
state: "normal"
})
const tabId = newWindow.tabs[0].id;
if (!newWindow.tabs[0].url) await onTabUrlUpdated(tabId);
const results = await chrome.scripting.executeScript({
target: {tabId},
function: setBackground,
});
});
setBackground() function to remove existing html and replace it, but keep url tab[0].url from active page to embed it as an iframe
function setBackground(){
chrome.storage.sync.get("data", ({ data }) => {
document.write(data.template);
//add tab[0].url as iframe
});
}
How can I replace the new window with my own html rather than render text?
One way of reading the contents of a local .html file with fetch would be the following:
function setBackground(){
try {
//fetch local asset
const res = await fetch(chrome.runtime.getURL("/assets/index.html"));
const myhtml = await res.text()
document.write(myhtml);
} catch(err){
console.log("fetch error", err);
}
}

Add PWA to home screen not working on chrome mobile

I recently got into front end developpement to make an interface for a nodejs server hosted on a raspberry pi.
I heard of progressive web app and wanted the user to be able to install it on his phone.
So here are the manifest and the service worker script.
Manifest:
{
"name": "Philips D6330",
"short_name": "Philips D6330",
"description": "A control app for the Philips D6330",
"icons": [
{
"src": "https://192.168.1.26/cdn/favicon.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "https://192.168.1.26",
"display": "fullscreen",
"orientation": "portrait",
"theme_color": "#333333",
"background_color": "#333333",
"scope": "/"
}
Service Worker:
const CACHE_NAME = 'cache';
const CACHE = [
'/',
'/cdn/offline.html',
'/cdn/style.css',
'/cdn/favicon.png',
'/cdn/linetoB.ttf',
'/cdn/linetoL.ttf',
'/cdn/neon.ttf',
'/cdn/not.png',
'/cdn/next.png',
'/cdn/pause.png',
'/cdn/play.png',
'/cdn/previous.png',
'/cdn/dots.png',
'/cdn/rfid.png'
];
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
return cache.addAll(CACHE)
})
.then(self.skipWaiting())
)
})
self.addEventListener('fetch', function(event) {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request)
.catch(() => {
return caches.open(CACHE_NAME)
.then((cache) => {
return cache.match('/cdn/offline.html');
})
})
);
}
else {
event.respondWith(
fetch(event.request)
.catch(() => {
return caches.open(CACHE_NAME)
.then((cache) => {
return cache.match(event.request)
})
})
);
}
})
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys()
.then((keyList) => {
return Promise.all(keyList.map((key) => {
if (key !== CACHE_NAME) {
console.log('[ServiceWorker] Removing old cache', key)
return caches.delete(key)
}
}))
})
.then(() => self.clients.claim())
)
})
I think also relevant to tell you that all of this happens on my local network so my node server uses https with a self-signed certificate made using this tutorial : https://medium.com/#tbusser/creating-a-browser-trusted-self-signed-ssl-certificate-2709ce43fd15
But even tho it's a self-signed certificate, the service worker seem to registers well on firefox and chrome, as it stores the files and displays the offline page when offline.
Here is my problem :
when I want to install it from the desktop version of chrome I can but not chrome mobile (or samsung internet)...
here is the piece of code i use to make it instalable :
<script defer>
window.addEventListener('beforeinstallprompt', (event) => {
console.log('👍', 'beforeinstallprompt', event);
// Stash the event so it can be triggered later.
window.deferredPrompt = event;
});
butInstall.addEventListener('click', () => {
console.log('👍', 'butInstall-clicked');
const promptEvent = window.deferredPrompt;
if (!promptEvent) {
// The deferred prompt isn't available.
return;
}
// Show the install prompt.
promptEvent.prompt();
// Log the result
promptEvent.userChoice.then((result) => {
console.log('👍', 'userChoice', result);
// Reset the deferred prompt variable, since
// prompt() can only be called once.
window.deferredPrompt = null;
});
});
window.addEventListener('appinstalled', (event) => {
console.log('👍', 'appinstalled', event);
});
</script>
It comes from here https://web.dev/codelab-make-installable/
Here is a screenshot of the before install prompt event with the lighthouse report if it can help (by the way the plus sign in the url shows it's working):
console log infos
But on mobile the plus sign doesn't show up and nothing happens when I click the button... And as I don't have acces to the console I can't see any errors...
------ Edit ------
After using alert to log what the console says, I think the problem comes from the service worker does register well because I get this :
"ServiceWorker registration failed: SecurityError: Failed to register a ServiceWorker for scope ('https://192.168.1.26/') with script ('https://192.168.1.26/sw.js'): An SSL certificate error occurred when fetching the script.".
Is there a way to make the browser trust my self-singed certificate ?
Any help, suggestion or comment is welcome ^^
Its a .webmanifest and you dont match the criteria. You need an 512x512 AND an 192x192 icon.
As start_url i would just use /?source=pwa
"icons": [
{
"src": "icon.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "big_icon.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": "/?source=pwa",
After that you need to link your webmanifest with <link>
<link rel="manifest" href="/manifest.webmanifest">
Check if the paths are all correct.
Here is my own little "library" to abstract things: https://github.com/ilijanovic/serviceHelper (its under development tho)
If you met all criteria, then you can make your app installable
Here its how it works with my library. You instantiate the class and pass the path to the service worker.
var sh = new ServiceHelper("./sw.js");
sh.init().then(response => {
console.log("Initialized");
})
sh.on("notinstalled", (e) => {
console.log("App is not installed");
//do some stuff. For example enable some button
//button.classList.remove("disabled");
})
butInstall.addEventListener('click', () => {
sh.installApp().then(e => {
// e = user choice event
console.log(e);
}).catch(err => {
// error if the app is already installed
console.log(err);
})