chrome.debugger.sendCommand() Input.dispatchMouseEvent error on MV3 - javascript

I'm struggling with this for hours now..
I'm currently writing a Chrome Extensions, its goal is to automate click on a website.
Since the website is checking for the isTrusted property, I have to emulate the click from chrome.debugger (Or at least, that the only way I found).
I have actually no one, but two problems.
1st one: If I set opts.x / opts.y "dynamically", it results in this error:
Uncaught (in promise) Error: {"code":-32602,"data":"Failed to deserialize params.x - BINDINGS: mandatory field missing at position 55","message":"Invalid parameters"}
2nd one: Tried putting the value directly, it's working, but it is not clicking on the provided coordinates, it's actually clicking lower.
This is my code:
background.js
chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) {
if (msg.text == "click that button please") {
let infos = []
chrome.debugger.attach({tabId: sender.tab.id}, "1.2", function() {
let clicked = false
let x = Math.floor(msg.button.offsetLeft + msg.button.offsetWidth / 2)
let y = Math.floor(msg.button.offsetTop + msg.button.offsetHeight / 2)
opts = {type: "mousePressed", button: "left", x: x, y: y, clickCount: 1}
chrome.debugger.sendCommand({tabId: sender.tab.id}, "Input.dispatchMouseEvent", opts)
.then((e) => {
infos.push(e)
});
opts.type = "mouseReleased"
chrome.debugger.sendCommand({tabId: sender.tab.id}, "Input.dispatchMouseEvent", opts)
.then((e) => {
infos.push(e)
});
})
sendResponse({clicked: infos});
} else {
sendResponse({error: 404});
}
return true;
});
content.js
var buttons = {
"market": "#app > div > div.mt-3 > div.d-flex.justify-content-between.align-items-baseline > div:nth-child(2) > button"
}
}
!async function() {
window.onclick = function(e) {
console.log(e.target, e)
}
let target = document.querySelector(buttons['market']); // Working
console.log(target);
chrome.runtime.sendMessage({text: "click that button please", button: target})
.then((result) => {
console.log('Did it clicked?', result); // result = []
});
}();
manifest.json
{
"name": "xxx",
"description": "xxx",
"version": "0.0.1",
"manifest_version": 3,
"author": "Me",
"host_permissions": [
"https://*.xxx.xxx/*"
],
"permissions": [
"debugger",
"activeTab",
"tabs",
"scripting"
],
"background": {
"service_worker": "background.js"
},
"content_scripts": [{
"matches": ["https://*.xxx.xxx/*"],
"js": ["content.js"]
}]
}

You can't send DOM elements via messages. It's not serializable, so an empty object arrives and your formula results in a NaN, i.e. not a number.
Solution: send an object/array with the element's coordinates.
Your use of offsetXXX props is incorrect, AFAICT, as the documentation for dispatchMouseEvent says x/y are relative to the viewport.
Solution: getBoundingClientRect().
const bb = elem.getBoundingClientRect();
chrome.runtime.sendMessage({x: bb.left, y: bb.top});
P.S. An alternative solution is to replace EventTarget.prototype.addEventListener or onclick setter of Document.prototype in the MAIN world of the page, so that your listener will call the site's listener passing a Proxy(event, handler) where handler returns true for isTrusted.

Related

Javascript Browser WebExtension API Manifest v3 - Service Worker and chrome.storage.local API issue

I'm implementing a small extension for Copy as cURL feature (as done by the Network tab of DevTools) and I would like to use Manifest v3. According to the documentation and to the contribution of the community, Service Worker at a certain time stops to live so some variables cannot retrieve the needed information from the active tab.
For managing this, I'm using chrome.storage.local.set and .get functions in order to keep the needed information also after the Service Worker stops to live. When I run the extension test, I don't receive any error, but, despite I retrieve the stored variables by the chrome.storage API, sometimes I continue to not retrieve the values anymore also when the Service Worker should be alive. For example:
when I connect to a website, I can retrieve and copy the correct data also in 1 min, then, if I continue to Copy (without refreshing the page), I don't get the parameters (i.e., GET headers).
sometimes, if I open a new tab, insert an address and quickly press Copy as cURL, of my extension, headers are not copied, and I need to refresh the page (not by clicking refresh button of browser but click on URL then ENTER) for getting them.
Maybe the issue is not related to the Time-to-live of the Service Worker because I can keep a page opened for a lot of minutes and it gives me the right parameters. I don't know where my approach is failing. The code of this small implementation is the following:
background.js
"use strict";
/*
Called when the item has been created, or when creation failed due to an error.
We'll just log success/failure here.
*/
function onCreated() {
if (chrome.runtime.lastError) {
console.log(`Error: ${chrome.runtime.lastError}`);
} else {
console.log("Item created successfully");
}
}
/*
Called when the item has been removed.
We'll just log success here.
*/
function onRemoved() {
console.log("Item removed successfully");
}
/*
Called when there was an error.
We'll just log the error here.
*/
function onError(error) {
console.log(`Error: ${error}`);
}
/*
Create all the context menu items.
*/
chrome.contextMenus.create({
id: "tools-copy",
//title: chrome.i18n.getMessage("menuItemToolsCopy"),
title: "Copy",
contexts: ["all"],
}, onCreated);
chrome.contextMenus.create({
id: "tools-copy-curl",
parentId: "tools-copy",
//title: chrome.i18n.getMessage("menuItemToolsCopyAsFFUF"),
title: "Copy as cURL",
contexts: ["all"],
}, onCreated);
const tabData = {};
const getProp = (obj, key) => (obj[key] || (obj[key] = {}));
const encodeBody = body => {
var data = '';
// Read key
for (var key in body.formData) { //body is a JSON object
data += `${key}=${body.formData[key]}&`;
}
data = data.replace(/.$/,"");
var body_data = `'${data}'`;
return body_data;
}
const FILTER = {
types: ['main_frame', 'sub_frame'],
urls: ['<all_urls>'],
};
const TOOLS = {
CURL: 'tools-copy-curl',
};
chrome.webRequest.onBeforeRequest.addListener(e => {
getProp(getProp(tabData, e.tabId), e.frameId).body = e.requestBody;
chrome.storage.local.set({tabData: tabData}, function() {
console.log('HTTP request saved');
});
}, FILTER, ['requestBody']);
chrome.webRequest.onBeforeSendHeaders.addListener(e => {
getProp(getProp(tabData, e.tabId), e.frameId).headers = e.requestHeaders;
chrome.storage.local.set({tabData: tabData}, function() {
console.log('HTTP request saved');
});
}, FILTER, ['requestHeaders']);
chrome.tabs.onRemoved.addListener(tabId => delete tabData[tabId]);
chrome.tabs.onReplaced.addListener((addId, delId) => delete tabData[delId]);
chrome.contextMenus.onClicked.addListener((info, tab) => {
chrome.storage.local.get(["tabData"], function(items) {
const data = items.tabData[tab.id]?.[info.frameId || 0] || {};
if (info.menuItemId === TOOLS.CURL) {
var txt_clip = `curl -u '${info.frameUrl || tab.url}'` +
(data.headers?.map(h => ` -H '${h.name}: ${h.value}'`).join('') || '') +
(data.body? ' --data_raw ' + encodeBody(data.body) : '');
}
chrome.tabs.sendMessage(tab.id,
{
message: "copyText",
textToCopy: txt_clip
}, function(response) {})
});
});
content.js
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if (request.message === "copyText") {
navigator.clipboard.writeText(request.textToCopy);
sendResponse({status: true});
}
}
);
manifest.json
{
"manifest_version": 3,
"name": "CopyAsCURL",
"description": "Copy as cURL test example.",
"version": "1.0",
"default_locale": "en",
"background": {
"service_worker": "background.js"
},
"permissions": [
"contextMenus",
"activeTab",
"cookies",
"webRequest",
"tabs",
"clipboardWrite",
"storage"
],
"host_permissions": [
"<all_urls>"
],
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": ["content.js"]
}
],
"icons": {
"16": "icons/menu-16.png",
"32": "icons/menu-32.png",
"48": "icons/menu-48.png"
}
}
I want also to thank #wOxxOm for the support on similar topic.

Chrome Extension - Obtaining Active Tab's DOM information through background script

I know there have been many questions related to this topic but none of them thus far have allowed me to figure this out in V3. When I run the following background.js, I only get undefined.
The goal of my extension, at least for this stage, is to scrape the active tab's DOM and extract the text content of all of the div elements.
My background.js page:
function getDOM() {
let htmlarr = [];
const pageDOM = document.getElementsByTagName('div');
for (i = 0; i < pageDOM.length; i++) {
htmlarr += pageDOM.innerHTML;
}
return Object.assign({}, htmlarr)
}
chrome.tabs.onActivated.addListener(activeInfo => {
let domRes = chrome.scripting.executeScript({
target: { tabId: activeInfo.tabId },
func: getDOM
})
console.log(domRes);
});
my manifest.json:
{
"name": "HTML Sourcer",
"description": "Extract HTML source code",
"version": "1.0",
"minimum_chrome_version": "10.0",
"manifest_version": 3,
"permissions": ["scripting", "tabs", "activeTab"],
"host_permissions": [
"*://*/*"
],
"background": {
"service_worker": "background.js"
}
}
Any help would be much appreciated. Thank you!
Problem 1
Per the documentation this API method returns a Promise when there's no callback parameter.
To get the value of the Promise, add await and mark the function as async:
chrome.tabs.onActivated.addListener(async info => {
let domRes = await chrome.scripting.executeScript({
target: {tabId: info.tabId},
func: getDOM,
}).catch(console.error);
if (!domRes) return;
console.log(domRes);
});
There's a catch because some tabs don't support injection e.g. the start tab or chrome:// tabs.
Problem 2
In JavaScript += doesn't work with arrays, but only with numbers/strings. Use htmlarr.push() instead. There's also no need to convert the array to an object via Object.assign.
Actually, let's rewrite getDOM using Array.from:
function getDOM() {
return Array.from(
document.getElementsByTagName('div'),
el => el.innerHTML
);
}

Preserve toggle between different tabs

I'm working on a Google Chrome Extension that you're supposed to be able to turn on and off using the symbol in the extension toolbar. This toggle should be universal, meaning that no matter where you turn it on or off, the current state is preserved everywhere. Doesn't matter in what tab or window you were, the status is shared everywhere.
Let's just say for our example, it's supposed to write "I'm on!" in the console when it's turned on and you press the A key. If it's turned off and you press A, it will say "I'm off!".
manifest.json:
{
"name": "Test Extension",
"version": "1.0",
"manifest_version": 2,
"description": "Just a test for Stack Overflow.",
"browser_action": {
"default_icon": "images/icon.png"
},
"background": {
"scripts": ["background.js"]
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
]
}
background.js:
var extensionMode = true;
chrome.browserAction.onClicked.addListener(function(tab) {
extensionMode = !extensionMode;
if(extensionMode) {
chrome.browserAction.setIcon({
path : "images/icon.png"
});
} else {
chrome.browserAction.setIcon({
path : "images/icon_disabled.png"
});
}
let msg = {
extensionMode: extensionMode
}
chrome.tabs.sendMessage(tab.id, msg);
});
content.js:
var extensionMode = true;
chrome.runtime.onMessage.addListener(gotMessage);
function gotMessage(message, sender, sendResponse) {
extensionMode = message.extensionMode;
}
onkeydown = onkeyup = function(e){
if(extensionMode) {
if(event.keyCode == 65) { // A
console.log("I'm on!");
}
} else {
if(event.keyCode == 65) { // A
console.log("I'm off!");
}
}
}
The above code works when you stay in the tab you are, but not when you switch... the icon will stay disabled, but extensionMode actually reverses back to true.
What am I doing wrong here? Is this the wrong approach for what I'm trying to do?
chrome.tabs.sendMessage targets just one tab so you would need to get a list of all tabs using chrome.tabs.query and send the message to each one:
const ignoreRuntimeError = () => chrome.runtime.lastError;
chrome.tabs.query({}, tabs => {
tabs.forEach(tab => chrome.tabs.sendMessage(tab.id, msg, ignoreRuntimeError));
});
You would also need to query the state in content scripts on tabs navigated/opened later:
chrome.runtime.sendMessage('getState', state => extensionMode = state);
For which the background script should have a listener:
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg === 'getState') {
sendResponse(extensionMode);
}
});
A more efficient approach for you to consider:
use a nonpersistent event page and store the state in chrome.storage.local
run the content scripts only when enabled using chrome.tabs.executeScript and register/unregister the DOM listeners when toggled

How to get selected text in the background script from the active tab (after a hotkey)?

I'm trying to get the selected text from a web page after a hotkey like for example Ctrl+SHIFT+Number. I'm starting with the code from Firefox help.
The manifest.json:
{
"description": "Native messaging example extension",
"manifest_version": 2,
"name": "Native messaging example",
"version": "1.0",
"icons": {
"48": "icons/message.svg"
},
"applications": {
"gecko": {
"id": "ping_pong#example.org",
"strict_min_version": "50.0"
}
},
"background": {
"scripts": ["background.js"]
},
"commands": {
"toggle-feature": {
"suggested_key": {
"default": "Ctrl+Shift+Y",
"linux": "Ctrl+Shift+0"
},
"description": "Send a 'toggle-feature' event"
}
},
"browser_action": {
"default_icon": "icons/message.svg"
},
"permissions": ["nativeMessaging"]
}
The JavaScript file:
/*
On startup, connect to the "ping_pong" app.
*/
var port = browser.runtime.connectNative("ping_pong");
/*
Listen for messages from the app.
*/
port.onMessage.addListener((response) => {
console.log("Received: " + response);
});
/*
On a click on the browser action, send the app a message.
*/
browser.browserAction.onClicked.addListener(() => {
console.log("Sending: ping");
port.postMessage("ping");
});
browser.commands.onCommand.addListener(function(command) {
if (command == "toggle-feature") {
console.log("toggling the feature!");
text1 = window.getSelection();
console.log(text1);
}
});
The debugger says:
Selection { anchorNode: null, anchorOffset: 0, focusNode: null,
focusOffset: 0, isCollapsed: true, rangeCount: 0, caretBidiLevel: null
}
The messaging works, the hotkey works, but I can't get the selected text. Is there another method which I need to use? I tried a lot of code all yesterday, but I didn't find how to do it. Sometimes I have another error from the debugger, but I can never get the selected text. It is a problem of focus? It is crazy!
I read the code from other add-ons. It seems they use that method but maybe it is in a popup window?
I'm on Debian Stretch, and Firefox 56. I tried on 2 computers.
To get the selected text you must use a content script. Given that you are initiating getting the selected text from a hotkey defined with a manifest.json commands, you're best off using tabs.executeScript() to inject the needed code when the user presses the hotkey.
The following adapts the code you have in the question to do only the portion which is defining the hotkey and adds getting the selection (based on the code in Get the Highlighted/Selected text) using tabs.executeScript() to inject into all frames in the activeTab.
It is possible for the user to have made a selection in each existing iframe. You will need to determine how you want to handle that. The code below gets the selection from each iframe. However, it currently discards all but the last selection found (the first result is the main frame). You may want to notify the user when they have selections in multiple frames. Note that Chrome does not permit selecting text in multiple frames, but Firefox does.
The following code is tested in both Firefox and Chrome.
manifest.json:
{
"description": "Get selected text upon hotkey",
"manifest_version": 2,
"name": "Hotkey: get selected text",
"version": "1.0",
"icons": {
"48": "icon.png"
},
"background": {
"scripts": ["background.js"]
},
"commands": {
"get-selected-text": {
"suggested_key": {
"default": "Ctrl+Shift+Y",
"linux": "Ctrl+Shift+0"
},
"description": "Get the selected text from the active tab."
}
},
"permissions": [
"activeTab"
]
}
background.js:
chrome.commands.onCommand.addListener(function (command) {
if (command == "get-selected-text") {
chrome.tabs.executeScript({
code: '(' + getSelectionText.toString() + ')()',
//We should inject into all frames, because the user could have made their
// selection within any frame, or in multiple frames.
allFrames: true,
matchAboutBlank: true
}, function (results) {
selectedText = results.reduce(function (sum, value) {
//This checks all the results from the different frames to get the one
// which actually had a selection.
if (value) {
if (sum) {
//You will need to decide how you want to handle it when the user
// has things selected in more than one frame. This case is
// definitely possible (easy to demonstrate).
console.log('Selections have been made in multiple frames:');
console.log('Had:', sum, ':: found additional:', value);
}
// Currently, we just discard what was obtained first (which will be
// the main frame). You may want to concatenate the strings, but
// then you need to determine which comes first. Reasonably, that
// means determining where the iframe is located on the page with
// respect to any other selection the user has made. You may want
// to just inform the user that they need to make only one
// selection.
return value;
}
return sum;
}, '');
console.log('selectedText:', selectedText);
})
}
});
//The following code to get the selection is from an answer to "Get the
// Highlighted/Selected text" on Stack Overflow, available at:
// https://stackoverflow.com/a/5379408
// The answer is copyright 2011-2017 by Tim Down and Makyen. It is
// licensed under CC BY-SA 3.0, available at
// https://creativecommons.org/licenses/by-sa/3.0/
function getSelectionText() {
var text = "";
var activeEl = document.activeElement;
var activeElTagName = activeEl ? activeEl.tagName.toLowerCase() : null;
if (
(activeElTagName == "textarea") || (activeElTagName == "input" &&
/^(?:text|search|password|tel|url)$/i.test(activeEl.type)) &&
(typeof activeEl.selectionStart == "number")
) {
text = activeEl.value.slice(activeEl.selectionStart, activeEl.selectionEnd);
} else if (window.getSelection) {
text = window.getSelection().toString();
}
return text;
}
I found a solution:
I select the text from any webpage, and I have the text in the background.js and after I can do what I want with the text. In my specific case, I use an external program (in python) to receive the selected text.
Manifest.json
{
"description": "Native messaging + Hotkey + content-script messaging",
"manifest_version": 2,
"name": "getSelectedTextFromHotkey",
"version": "1.0",
"icons": {
"48": "icons/message.svg"
},
"applications": {
"gecko": {
"id": "gettext#example.org",
"strict_min_version": "50.0"
}
},
"background": {
"scripts": ["background.js"]
},
"commands": {
"toggle-feature": {
"suggested_key": {
"default": "Ctrl+Shift+4",
"linux": "Ctrl+Shift+5"
},
"description": "Send the selected text"
}
},
"browser_action": {
"default_icon": "icons/message.svg"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content-script.js"]
}
],
"permissions": [ "<all_urls>","nativeMessaging","webRequest"]
}
Background.js
var port = browser.runtime.connectNative("gettext");
browser.runtime.onConnect.addListener(connected);
port.onMessage.addListener((response) => {
console.log("Received: " + response);
});
function onExecuted(result) {
console.log(`We executed`);
}
function onError(error) {
console.log(`Error: ${error}`);
}
browser.commands.onCommand.addListener(function(command) {
if (command == "toggle-feature") {
console.log("toggling the feature!");
var executing = browser.tabs.executeScript({ file: "/content-script.js", allFrames: false });
executing.then(onExecuted, onError);
}
});
var portFromCS;
function connected(p) {
portFromCS = p;
portFromCS.onMessage.addListener(function(m) {
console.log("message selected:")
console.log(m);
console.log("Sending: ping");
port.postMessage("ping");
});
}
content-script.js
// content-script.js
var selectedText = getSelection().toString();
var myPort = browser.runtime.connect({name:"port-from-cs"});
myPort.postMessage(selectedText);
gettext.json
{
"name": "gettext",
"description": "Native messaging + Hotkey + content-script messaging",
"path": "/home/marie/web-ext/gettext.py",
"type": "stdio",
"allowed_extensions": [ "gettext#example.org" ]
}
gettext.py
#!/usr/bin/python -u
# Note that running python with the `-u` flag is required on Windows,
# in order to ensure that stdin and stdout are opened in binary, rather
# than text, mode.
import sys, json, struct
# Read a message from stdin and decode it.
def getMessage():
rawLength = sys.stdin.read(4)
if len(rawLength) == 0:
sys.exit(0)
messageLength = struct.unpack('#I', rawLength)[0]
message = sys.stdin.read(messageLength)
return json.loads(message)
# Encode a message for transmission, given its content.
def encodeMessage(messageContent):
encodedContent = json.dumps(messageContent)
encodedLength = struct.pack('#I', len(encodedContent))
return {'length': encodedLength, 'content': encodedContent}
# Send an encoded message to stdout.
def sendMessage(encodedMessage):
sys.stdout.write(encodedMessage['length'])
sys.stdout.write(encodedMessage['content'])
sys.stdout.flush()
# BE CAREFUL, NEVER USE THE CONSOLE in the loop ! it stops the connection!!!
while True:
receivedMessage = getMessage()
if (receivedMessage == "ping"):
sendMessage(encodeMessage("pong"))
It seems to work well on Firefox.

Copy Data from webpage Firefox extension

I have a very strange situation. I have an extension which copies stuff from the webpage based on the user's selection. But, when ever there are multiple frames its fails. For example on Gmail. If I select anything from Gmail and try to find the selection it will end up with an error:
Error: window.getSelection(...) is null
Here is my code (This is a working example. I didn't include the icon.):
manifest.json
{
"description": "Adds a solid red border to all webpages matching mozilla.org. See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Examples#borderify",
"manifest_version": 2,
"name": "Borderify",
"version": "1.0",
"homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/borderify",
"icons": {
"48": "icons/border-48.png"
},
"background": {
"scripts": ["myaddone.js"]
},
"browser_action": {
"default_icon": "icons/trash.svg",
"default_title": "Forget it!"
},
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["callHere.js"],
"all_frames": true
}]
}
callHere.js
function logger(msg) {
console.log("=============");
console.log(msg);
console.log("=============");
}
var getSelectedDataFromPage = function () {
logger("fooo");
selec = window.getSelection().toString().trim();
return selec;
}
browser.runtime.onMessage.addListener(request => {
var messageToCopy = request.greeting;
if (messageToCopy == "findCopy") {
var selectedText = getSelectedDataFromPage();
return Promise.resolve({
response: selectedText
});
}
logger(messageToCopy);
return Promise.resolve({
response: "Fail"
});
});
myaddone.js
function logger(msg) {
console.log(msg);
}
function onError(error) {
console.error(`Error: ${error}`);
}
function findSelectionTExt(tabs) {
for (let tab of tabs) {
browser.tabs.sendMessage(tab.id, {
greeting: "findCopy"
}).then(response => {
logger(response.response);
}).catch(onError);
}
}
browser.browserAction.onClicked.addListener(() => {
browser.tabs.query({
currentWindow: true,
active: true
}).then(findSelectionTExt).catch(onError);
});
It is using a message system to content script to copy stuff from selection. It works perfectly fine with Stack Overflow and other sites, but not sites which use more frames etc., like Gmail.
Loop Image, as you can see it able to grab the text first time and then its keep sending the message I think.
What I am really missing?
I did solved my issue using context menu item and it works very well with every where like iframes. I got a example from firefox repo. https://github.com/mdn/webextensions-examples/tree/master/context-menu-demo.

Categories