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.
Related
I am attempting to display an alarm that pops up in my browser from background.js in Manifest v3. However, using the code implementation that is described in the Manifest v3 page does not produce an alarm.
Manifest.js:
{
"name": "Focus-Bot",
"description": "A bot meant to help you focus",
"version": "1.0",
"manifest_version": 3,
"background": {
"service_worker": "background.js"
},
"permissions": ["storage", "activeTab", "scripting"],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "/images/get_started16.png",
"32": "/images/get_started32.png",
"48": "/images/get_started48.png",
"128": "/images/get_started128.png"
}
},
"icons": {
"16": "/images/get_started16.png",
"32": "/images/get_started32.png",
"48": "/images/get_started48.png",
"128": "/images/get_started128.png"
},
"options_page": "options.html"
}
Background.js:
chrome.runtime.onInstalled.addListener(() => {
chrome.scripting.executeScript({
function: showAlert
})
});
function showAlert(){
alert('object input for later');
}
This version of background.js returns the following error
TypeError: Error in invocation of scripting.executeScript(scripting.ScriptInjection injection, optional function callback): Error at parameter 'injection': Missing required property 'target'.
The example code of a working Chrome Extension (the green background button) uses chrome.tabs in a popup.js file to get a target and inject javascript, but when background.js runs the same code like this:
Background.js (tabs):
chrome.runtime.onInstalled.addListener(() => {
let [tab] = await chrome.tabs.query(queryOptions);
console.log(tab)
chrome.scripting.executeScript({
function: showAlert
})
});
function showAlert(){
alert('object input for later');
}
Background.js seems to crash with "Service worker registration failed", with no error logs.
How do I display an alarm for the current active page from background.js?
As the error message says you need to add target to executeScript's parameters. Always look up the exact usage of API methods in the documentation.
Your code uses await but the function isn't declared with async which is a syntax error that causes the service worker to fail the registration. Currently ManifestV3 is riddled with bugs so it doesn't even show the cause of the failure so you'll have to use try/catch manually.
try {
chrome.runtime.onInstalled.addListener(async () => {
const [tab] = await chrome.tabs.query(queryOptions);
chrome.scripting.executeScript({
target: {tabId: tab.id},
function: showAlert,
});
});
} catch (e) {
console.error(e);
}
An arguably better/cleaner approach would be to use two files: the main code in bg.js and the try-catch wrapper in bg-loader.js that imports bg.js, see this example.
Note that the active tab may be un-injectable e.g. a default start page or a chrome:// page (settings, bookmarks, etc.) or a chrome-extension:// page. Instead you can open a small new window:
alert({html: 'Foo <b>bar</b><ul><li>bla<li>bla</ul>'})
.then(() => console.log('alert closed'));
async function alert({
html,
title = chrome.runtime.getManifest().name,
width = 300,
height = 150,
left,
top,
}) {
const w = left == null && top == null && await chrome.windows.getCurrent();
const w2 = await chrome.windows.create({
url: `data:text/html,<title>${title}</title>${html}`.replace(/#/g, '%23'),
type: 'popup',
left: left ?? Math.floor(w.left + (w.width - width) / 2),
top: top ?? Math.floor(w.top + (w.height - height) / 2),
height,
width,
});
return new Promise(resolve => {
chrome.windows.onRemoved.addListener(onRemoved, {windowTypes: ['popup']});
function onRemoved(id) {
if (id === w2.id) {
chrome.windows.onRemoved.removeListener(onRemoved);
resolve();
}
}
});
}
To begin, I would like to note that I am a complete extension noob, so please forgive me if this is a silly question. I have been working on this for a couple days and have found nothing on the web that helps with this particular problem.
Here is what I am trying to do:
Say that a user is browsing a site that displays part numbers and info. Each time the user clicks on something from the site, the site loads new information and the URL changes. The extension then grabs the UID (unique identifier) from the URL and searches a database for that UID. If the UID is found in the database, a message is returned to the extension. From here, I would like to write a div to the page saying "This part is already found in the database".
The problem here appears to be that the page changes, but it's not a new page... it's just new information. I can get the extension to write to the page on initial load, but when the page changes, nothing is written.
Here is the relevant code that I am using:
Manifest
{
"manifest_version": 2,
"name": "My Extension",
"version": "1.0",
"description": "An extension to write to a page",
"icons": {
"128": "icon128.png",
"48": "icon48.png",
"16": "icon16.png"
},
"browser_action": {
"default_icon": "icon16.png",
"default_popup": "popup.html"
},
"background": {
"scripts":["./js/jquery.min.js","./js/extension-check.js"]
},
"content_scripts": [{
"js": ["./js/jquery.min.js","./js/extension-content.js"],
"matches": ["https://www.example.com/*"]
}],
"permissions": [
"activeTab",
"tabs",
"http://*/*",
"https://*/*"
]
}
The popup.html does nothing but give some buttons that the user can click and go to some sites. It has no relevant code.
extension-check.js
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
chrome.tabs.query({'active': true, 'lastFocusedWindow': true}, function (tabs) {
if (tabs === undefined || tabs[0] === undefined) return;
var url = tabs[0].url;
if (url.indexOf("example.com/specific-directory") > 0) {
var part_url = // get part of the url
var search_url = 'myexample.com/searching?' + part_url;
$.ajax({
url: search_url,
type: 'post',
dataType: 'JSON',
success: async function(data, textStatus, jqXHR)
{
if (Object.keys(data).length != 0 && data.constructor != Object) {
// Because this script is running in the
// background, I need to send the command to
// something that has permissions to write to the page
chrome.tabs.executeScript(tabId, {
file: './js/extension-content.js'
}, function() {
chrome.tabs.sendMessage(tabId, {parameter: data});
});
}
},
error: function(jqXHR, textStatus, errorThrown)
{
console.log(jqXHR);
console.log(textStatus);
console.log(errorThrown);
}
});
}
}
});
});
extension-content.js
(function() {
chrome.runtime.onMessage.addListener(function(message) {
var receivedParameter = message.parameter;
alert ("In this - " + receivedParameter);
$(".core").prepend("<div>This is a test - " + receivedParameter + "</div>");
});
})();
NOTE: If I change this page to:
$(document).ready(function() {
$(".core").prepend("<div>This is a test</div>");
});
The div IS written to the page. But, this is written to the page once and never changes or goes away. This would display incorrect information.
The ajax is working perfectly... The alert (which is only there for a test) does display and displays with the passed parameter. However, NOTHING is written to the page.
Can someone please help me? In addition, if I have something wrong in the manifest (such as permissions), I would be ecstatic for some feedback. Again, noob at this, so I may have added that I don't need, or don't have things that I do need.
Thanks in advance.
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.
I'm building a chrome extension which communicates with a nodejs server through websockets. The point of it is to track browsing history with content. It all seems to work, but occasionally (30% of the time) the callback in a function passed to onMessage.addListener doesn't fire correctly. Let me show you the code:
background.js
var socket = io('http://localhost:3000/');
var tabLoad = function (tab) {
socket.emit('page load', tab);
};
var tabUpdate = function (tabid, changeinfo, tab) {
var url = tab.url;
if (url !== undefined && changeinfo.status == "complete") {
tab.user_agent = navigator.userAgent;
tab.description = '';
tab.content = '';
socket.emit('insert', tab);
}
};
socket.on('inserted', function(page){
socket.emit('event', 'Requesting page content\n');
//page = {tab: page, id: docs._id};
chrome.tabs.sendMessage(page.tab_id, {requested: "content", page: page}, function(data) {
socket.emit('content', data);
});
});
try {
chrome.tabs.onCreated.addListener(tabLoad);
chrome.tabs.onUpdated.addListener(tabUpdate);
} catch(e) {
alert('Error in background.js: ' + e.message);
}
content script - public.js
var messageHandler = function(request, sender, sendContent) {
if (request.requested == "content") {
var html = document.getElementsByTagName('html')[0].innerHTML;
var data = {
content: html,
page: request.page
};
sendContent(data);
return true;
}
};
chrome.extension.onMessage.addListener(messageHandler);
The problem is that sometimes data in sendContent is undefined, while sometimes it is alright. Any ideas how to debug this or what i'm doing wrong?
I've tried replacing document.getElementsByTagName('html')[0].innerHTML with a hardcoded 'test' string, but that didn't help.
Pages like youtube/wikipedia seem to never work, while facebook/google works.
Edit: The sendContent callback does fire 100% of the time it's just that the data passed to it is undefined.
Edit: Here's the manifest file
{
"manifest_version": 2,
"name": "Socket test",
"description": "sockets are cool",
"version": "1.0",
"permissions": [
"http://st-api.localhost/",
"http://localhost:3000/",
"tabs",
"background",
"history",
"idle",
"notifications"
],
"content_scripts": [{
"matches": ["*://*/"],
"js": ["public/public.js"]
//"run_at": "document_start"
}],
//"browser_action": {
// "default_icon": "logo.png",
// "default_popup": "index.html"
//},
"background": {
//"page" : "background.html",
"scripts": ["socket-io.js", "background.js"],
"persistent": true
}
}
First off, your understanding that sendContent is executed 100% of the time is wrong.
As established in the comments, the sendMessage callback also gets executed when there was an error; and this error is, in your case, "Receiving end does not exist"
The error lies in your manifest declaration of the content script. A match pattern "*://*/" will only match top-level pages on http and https URIs. I.e. http://example.com/ will match, while http://example.com/test will not.
The easiest fix is "*://*/*", but I would recommend the universal match pattern "<all_urls>".
With that fixed, there are still a couple of improvements to your code.
Replace chrome.extension.onMessage (which is deprecated) and use chrome.runtime.onMessage
Modify the sendMessage part to be more resilient, by checking for chrome.runtime.lastError. Despite the wide permission, Chrome still won't inject any content scripts into some pages (e.g. chrome:// pages, Chrome Web Store)
Make sure you use "run_at" : "document_start" in your content script, to make sure onUpdated with "complete" is not fired before your script is ready.
I have a background script in my extension that creates a context menu item and handles it. When it is clicked, a cookie is created with specific details. Here is the source for that file:
script.js
function createC() {
var x = 1;
var y = 2;
//Create Cookie
document.cookie = document.URL + "=" + " " + x + " " + y + " ; " + 4102444799;
console.log("Cookie Created.");
}
chrome.contextMenus.create({
title: "Create Cookie",
contexts:["all"],
onclick: createC,
});
Obviously the variables used in it are for testing. When I run document.cookie; in the console, the cookie does not appear. I have tried using the chrome.cookies API and had the same issue.
Does the cookie not appear because it is created in the background script? I am trying to set it on the current tab the user is on, not the background page itself.
manifest.json
{
"manifest_version": 2,
"name": "MyExtension",
"description": "Do stuff",
"version": "0.1",
"icons": { "16": "icon.png",
"48": "icon.png",
"128": "icon.png" },
"options_page": "options.html",
"permissions": [
"tabs", "<all_urls>", "contextMenus", "cookies"
],
"background": {
"scripts": ["script.js"]
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["nav.js"]
}
]
}
In background script, 'document' is not for the current page, but for the extension background page(chrome-extension://[your extension id]/bacground.html). So, you can't use 'document.cookie', you need to try chrome.cookies.get like this:
/**
* Create cookie for the special page
* #param {Object<key, value>} detail
* #param {Function=} opt_callback
*/
function createCookie(detail, opt_callback) {
chrome.cookies.set(detail, opt_callback);
}
You need to use javascript code in specific tab if you want to use the document the current page instead of background.html.
This can be done by function executeScript, your syntax is :
chrome.tabs.executeScript( tabId, details, callback )
chrome.tabs.executeScript( MyTabIdNumberMandatoryInYourCase, MyScriptCodeInLineOrUrl, MyCallbackOptional )
tabId matches the ID of the active tab page, background.js file is executed under the main background.html, then you need to pass the correct ID if you do not pass it, and hopefully it will execute the background.html as the active tab.
All WebRequest events, has a variable called details and she carries a tabid value, and you access it via details.tabId, below is a code that I use in one of my extensions already created.
var onCompletedExecuteScriptDetails = {
// You can run all the code in the inline form,
// rather than using the parameter "file", use the "code" parameter, but is very ugly,
// is much more elegant to use the "file" mode
// my-script.js is a file with code to create cookie
file : "my-script.js"
};
var onCompletedExecuteScript = function ( details ) {
chrome.tabs.executeScript( details.tabId, onCompletedExecuteScriptDetails );
};
var onCompletedCallback = function ( details ) {
document.addEventListener( 'DOMContentLoaded', onCompletedExecuteScript( details ) );
};
var onCompletedFilter = {
urls : [
"http://*/*",
"https://*/*"
]
};
chrome.webRequest.onCompleted.addListener( onCompletedCallback, onCompletedFilter, onCompletedInfo );
executeScript
I ended up using:
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
if (changeInfo.url != null) {
url = changeInfo.url;
}
});
This is what I ended up doing.
In manifest.json:
"permissions": [
"cookies",
"*://*.target_website.com/",
"*://*/_generated_background_page.html"
]
In background.js
chrome.cookies.set({
"name": "cookie's name",
"url": "the URL you want to apply the cookies to",
"value": "cookie's value"
}, function(cookie) {
if (chrome.extension.lastError) {
console.log(chrome.extension.lastError);
}
if (chrome.runtime.lastError) {
console.log(chrome.runtime.lastError);
}
});