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.
Related
I have created a google extension that calls my API to get if there is new cookies added to the API and that API would then transfer it into my chrome browser. E.g.
The problem is that whatever site I actually try to access, it does quite alot calls to my API which I am scared of where it will could eventually overload the database and my manifest.json looks like this:
{
"name": "Cookie Injector",
"version": "0.1",
"manifest_version": 3,
"background": {
"service_worker": "./js/background.js"
},
"permissions": ["cookies", "storage", "tabs"],
"action": {
"default_popup": "./html/popup.html"
},
"host_permissions": ["<all_urls>"],
"icons": {
"16": "/images/icon.png",
"32": "/images/icon.png",
"48": "/images/icon.png",
"128": "/images/icon.png"
}
}
and my background.js is:
function get_pending_cookies(discord_id) {
const id = new URLSearchParams({ 'user_id': discord_id });
return fetch(`http://cookie.pynewbie.com/pending-cookies?${id}`, {
method: 'GET',
}).then((response) => {
return response.json();
}).then((json) => {
return json;
});
}
function handle_cookies(discord_id) {
get_pending_cookies(discord_id).then((result) => {
...
// detect is focused on correct page
chrome.tabs.query({ currentWindow: true, active: true }, function (tabs) {
// getting current tab's URL
if (!(tabs === undefined || tabs.length === 0)) {
if (tabs[0].url === url) {
console.log("USER OPENED CORRECT PAGE, RELOADING");
chrome.tabs.reload(tabs[0].id);
}
}
});
})
});
}
chrome.runtime.onMessage.addListener(
function (request, sender, sendResponse) {
sendResponse(request);
set_discord_id(request.discord_id);
}
);
chrome.tabs.onUpdated.addListener(() => {
handle_cookies;
});
My question is how can I make the API (get_pending_cookies) to only be called when I enter specific URL e.g. pythonewbie.com (then it should do the API calls otherwise skip)
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.
I had this idea for a new chrome extension:
It should create a linking QR code for the currently open website, displayed in the popup.html which can be easily scanned with the smartphone. If you watch a video on YouTube, the current time of the video should be also embedded in the QR code, which makes it possible to continue watching the video directly in the YouTube app on the smartphone.
So far so good. But now I have the following problem:
This extension works fine on all websites. Only on Youtube there seems to be a problem with the asynchrony of the onmessage listener and the sending of the message to the contentScript (requesting the current time of the viewed YouTube video).
In the debugging console I get the following errors:
Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.
... getting this after trying to execute line 7 of background.js
Error handling response: TypeError: Cannot read property 'videoTime' of undefined
at chrome-extension://...../background.js:8:70
... getting this after trying to execute line 8 of background.js
popup.js
$(function() {
chrome.runtime.sendMessage({text: 'sendURL'}, function(response) {
$('#qr-code').attr('src', getQRCodeImgURL(response.url));
});
});
function getQRCodeImgURL(url) {
var qrCodeURL = new URL('http://api.qrserver.com/v1/create-qr-code/');
qrCodeURL.searchParams.set('data', encodeURI(url));
qrCodeURL.searchParams.set('size', '200x200');
return qrCodeURL;
}
background.js
chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
if (message.text == 'sendURL') {
chrome.tabs.query({active: true, /* lastFocusedWindow: true */}, function (tabs) {
var currentURL = new URL(tabs[0].url);
if (currentURL.href.indexOf('youtube.com/watch?v=') >= 0) { // if current website is youtube
chrome.tabs.sendMessage(tabs[0].id, { text: 'sendVideoTime' }, function (response) {
const ytVideoTime = timeStringToSeconds(response.videoTime);
var ytURL = new URL('https://youtu.be/');
ytURL.pathname = '/' + currentURL.searchParams.get('v');
ytURL.searchParams.set('t', ytVideoTime);
currentURL = ytURL;
sendResponse({ url: currentURL.href });
});
} else {
sendResponse({ url: currentURL.href });
}
});
}
return true;
});
function timeStringToSeconds(timeString) {
var seconds = 0;
var hms = timeString.split(':');
if (hms.length == 3) {
seconds = parseInt(hms[0])/* hours */ * 60 /* minutes per hour */ * 60 /* seconds per minute */;
hms.shift(); /* remove first element, for accessing first element in next step (also if hms doesnt is in this hh:mm:ss format) */
}
return seconds + (parseInt(hms[0]) * 60) /* seconds per minute */ + parseInt(hms[1]) /* seconds */;
}
contentScript.js
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if (message.text == 'sendVideoTime') {
const time = document.evaluate('//*[#id="movie_player"]/div[27]/div[2]/div[1]/div[1]/span[1]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.textContent;
sendResponse({videoTime: time});
}
return true;
});
manifest.json
{
"manifest_version": 2,
"name": "URL-QR-Code-Creator",
"version": "1.0",
"description": "This extension creates a linking QR code for the currently open website, which can be easily scanned with the smartphone. If you watch a video on the YouTube website, the current time of the video is also embedded in the QR code, which makes it possible to continue watching the video directly in the YouTube app on the smartphone.",
"icons": {
"16": "images/qr-code-16px.png",
"32": "images/qr-code-32px.png",
"48": "images/qr-code-48px.png",
"64": "images/qr-code-64px.png",
"128": "images/qr-code-128px.png"
},
"browser_action": {
"default_icon": {
"16": "images/qr-code-16px.png",
"32": "images/qr-code-32px.png",
"48": "images/qr-code-48px.png",
"64": "images/qr-code-64px.png",
"128": "images/qr-code-128px.png"
},
"default_title": "show QR-Code",
"default_popup": "popup.html"
},
"background": {
"scripts": [
"background.js"
],
"persistant": false
},
"content_scripts": [
{
"matches": ["*://www.youtube.com/watch?v=*"],
"js": ["contentScript.js"]
}
],
"permissions": [
"activeTab",
"tabs"
]
}
I did it!
At first I said goodbye to the idea of regulating access to the youtube DOM via an extra content script.
So I deleted contentScript.js and deleted it from manifest.json.
Now I simply accessed the DOM using the chrome.tabs.executeScript method in the background.js file and got the result using the callback function. Quite simply - without having to send messages around all the time.
I want to write a chrome extension which records the current active tab URL every time a new site is loaded and send it to a server for further use. So far I have managed to write the following code:
manifest.json
{
"manifest_version": 2,
"name": "Currenturl",
"description": "Fetches current tab url.",
"version": "0.1",
"author": "Tarun Khare",
"browser_action": {
"default_icon": "icon.png",
"default_title": "Just observing your current url."
},
"permissions": ["tabs", "activeTab"],
"background": {
"scripts": ["content.js"],
"persistent": false
}
}
content.js
chrome.tabs.query({'active': true, 'lastFocusedWindow': true}, function (tabs) {
var url = tabs[0].url;
console.log("hello: "+url);
});
I am using background scripts since chrome.tabs doesn't work in content scripts. But this extension is not printing anything in chrome console. What is the issue?
Rename content.js to background.js since this is a background script
Use chrome.tabs.onUpdated listener
Look at the correct console: Where to read console messages from background.js?
chrome.tabs.onUpdated.addListener((tabId, change, tab) => {
if (change.url) {
console.log(change.url);
}
});
It'll report the URL changes in all tabs.
You can also limit the processing to only the active tab by adding a check for tab.active property.
i try to write code based on the information you provide.
const tabUpdatelistenerFun = (tabid, changeInfo, tab) => {
const url = changeInfo.url;
if (!url || ['chrome://', 'about://'].some(p => url.startsWith(p))) return false;
const { index, active, highlighted, windowId } = tab;
if (!active) return false;
chrome.tabs.query({ index, highlighted, windowId, lastFocusedWindow: true }, () => {
console.log(url);
})
}
chrome.tabs.onUpdated.addListener(tabUpdatelistenerFun);
i think this is what you want.
You can count and extract the URL with full detail in background.js
here is the main code from below GihHub repository:
chrome.windows.getAll({ populate: true }, function (windows) {
windows.forEach(function (window) {
window.tabs.forEach(function (tab) {
//i++
collect all of the urls here, I will just log them instead
console.log("tab.ur[![enter image description here][1]][1]l aaaaakahari 2");
console.log(tab.url);
});
});
});
There is a GitHub repository of chrome Extension below here, you can find in the background.js there are methods to count and extract the open URL, and there is and even section that console it imminently when the user open or close any tab.
https://github.com/Farbod29/extract-and-find-the-new-tab-frome-the-browser-with-chrome-extention
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.