Chrome extension mv3 - catch zip files on anchor click - javascript

I put two pieces of code.
The first contains the chrome extension manifest version 2 files.
Here if I click on anchor with href pointed to zip file, then extension redirect to page from extension.
This is a worked example.
I am trying to achieve this for chrome extension with manifest version 3.
This is a second pieces of code.
First part.
Extension manifest version 2
manifest.json
{
"name": "Test app mv2",
"version": "0.1",
"manifest_version": 2,
"description": "test mv2",
"background": {
"scripts": [
"background.js"
]
},
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"icons": {
"128": "128.png"
},
"permissions": [
"webRequest",
"webRequestBlocking",
"<all_urls>"
],
"web_accessible_resources": [
"web/main.html"
]
}
background.js
function getHeaderFromHeaders(headers, headerName) {
for (var i=0; i<headers.length; ++i) {
var header = headers[i];
if (header.name.toLowerCase() === headerName) {
return header;
}
}
}
function isAllowed(details) {
var header = getHeaderFromHeaders(details.responseHeaders, 'content-type');
if (header) {
var headerValue = header.value.toLowerCase().split(';',1)[0].trim();
var mimeTypes = [
'application/zip'
];
return (mimeTypes.indexOf(headerValue) !== -1);
}
}
chrome.webRequest.onHeadersReceived.addListener(
function(details) {
if (details.method !== 'GET') {
// Don't intercept POST requests until http://crbug.com/104058 is fixed.
return;
}
if (!isAllowed(details)) {
return;
}
return { redirectUrl: chrome.runtime.getURL('web/main.html') };
},
{
urls: [
'<all_urls>'
],
types: ['main_frame', 'sub_frame']
},
['blocking','responseHeaders']
);
Full source for mv2
Second part.
Extension manifest version 3
manifest.json
{
"name": "Test app mv3",
"manifest_version": 3,
"version": "0.1",
"background": {
"service_worker": "./background.js"
},
"action": {
"default_title": "SW3"
},
"host_permissions": [
"<all_urls>"
],
"permissions": [
"webRequest",
"declarativeNetRequest",
"declarativeNetRequestFeedback",
"declarativeNetRequestWithHostAccess"
],
"web_accessible_resources": [{
"resources": ["web/main.html"],
"matches": ["<all_urls>"]
}],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
}
}
background.js
function getHeaderFromHeaders(headers, headerName) {
for (var i=0; i<headers.length; ++i) {
var header = headers[i];
if (header.name.toLowerCase() === headerName) {
return header;
}
}
}
function isAllowed(details) {
var header = getHeaderFromHeaders(details.responseHeaders, 'content-type');
if (header) {
var headerValue = header.value.toLowerCase().split(';',1)[0].trim();
var mimeTypes = [
'application/zip'
];
return (mimeTypes.indexOf(headerValue) !== -1);
}
}
chrome.webRequest.onHeadersReceived.addListener(
function(details) {
if (details.method !== 'GET') {
return;
}
if (!isAllowed(details)) {
return;
}
chrome.declarativeNetRequest.updateSessionRules({
addRules: [{
'id': 2001,
'priority': 1,
'action': {
'type': 'redirect',
'redirect': {
url: chrome.runtime.getURL('web/main.html')
}
},
'condition': {
'urlFilter': details.url,
'resourceTypes': ['main_frame']
}
}],
removeRuleIds: [2001]
});
return { redirectUrl: chrome.runtime.getURL('web/main.html') };
},
{
urls: [
'<all_urls>'
],
types: ['main_frame', 'sub_frame']
},
['responseHeaders']
);
Full source for mv3
For extension with mv3, above code achieved similar action as code for mv2.
The difference is that: when I click on anchor that pointed to zip file, then on the first click the dialog "save as" is shown and if I click on same zip anchor for second time, then redirect occurs.
For other zip files above actions are repeated.
How I can modify mv3 code to achieve same results as mv2?

This is still possible, but requires abandoning the chrome.webRequest API in favor of chrome.debugger. This allows your extension talk to the CDP protocol, where you can hook into the request/response lifecycle.
The first step is to attach the debugger the tabId in question, and send enable commands (if required) for the APIs you are using. Here you can find many examples of how to do this. For example, let's assume we want to use Fetch, so we need to send Fetch.enable.
Now, if you want to modify a request/response before it is processed, on a high level you need to:
Set up a handler for Fetch.requestPaused, configuring your target URL patterns (or * for all).
Check the request stage as described in the doc.
If Request: modify headers, and send Fetch.continueRequest.
If Response: Get the response body by sending Fetch.getResponseBody. If desired, modify body/status/headers. Fulfill the response to client by sending Fetch.fulfillRequest.
Helpful:
https://grep.app/search?q=chrome.debugger.sendCommand
https://grep.app/search?q=Fetch.fulfillRequest
I am using this approach successfully in a chrome extension project. MV3 killed the native extension request API (or at least, severely limited it) so for now this is the only option I am aware of. The downside is, an annoying bar appears under the tab, saying this browser is currently being debugged by $EXT_NAME.

Related

Make Chrome extension script target activeTab DOM [duplicate]

I'm trying to access the activeTab DOM content from my popup. Here is my manifest:
{
"manifest_version": 2,
"name": "Test",
"description": "Test script",
"version": "0.1",
"permissions": [
"activeTab",
"https://api.domain.com/"
],
"background": {
"scripts": ["background.js"],
"persistent": false
},
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"browser_action": {
"default_icon": "icon.png",
"default_title": "Chrome Extension test",
"default_popup": "index.html"
}
}
I'm really confused whether background scripts (event pages with persistence: false) or content_scripts are the way to go. I've read all the documentation and other SO posts and it still makes no sense to me.
Can someone explain why I might use one over the other.
Here is the background.js that I've been trying:
chrome.extension.onMessage.addListener(
function(request, sender, sendResponse) {
// LOG THE CONTENTS HERE
console.log(request.content);
}
);
And I'm just executing this from the popup console:
chrome.tabs.getSelected(null, function(tab) {
chrome.tabs.sendMessage(tab.id, { }, function(response) {
console.log(response);
});
});
I'm getting:
Port: Could not establish connection. Receiving end does not exist.
UPDATE:
{
"manifest_version": 2,
"name": "test",
"description": "test",
"version": "0.1",
"permissions": [
"tabs",
"activeTab",
"https://api.domain.com/"
],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
],
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"browser_action": {
"default_icon": "icon.png",
"default_title": "Test",
"default_popup": "index.html"
}
}
content.js
chrome.extension.onMessage.addListener(
function(request, sender, sendResponse) {
if (request.text && (request.text == "getDOM")) {
sendResponse({ dom: document.body.innerHTML });
}
}
);
popup.html
chrome.tabs.getSelected(null, function(tab) {
chrome.tabs.sendMessage(tab.id, { action: "getDOM" }, function(response) {
console.log(response);
});
});
When I run it, I still get the same error:
undefined
Port: Could not establish connection. Receiving end does not exist. lastError:30
undefined
The terms "background page", "popup", "content script" are still confusing you; I strongly suggest a more in-depth look at the Google Chrome Extensions Documentation.
Regarding your question if content scripts or background pages are the way to go:
Content scripts: Definitely
Content scripts are the only component of an extension that has access to the web-page's DOM.
Background page / Popup: Maybe (probably max. 1 of the two)
You may need to have the content script pass the DOM content to either a background page or the popup for further processing.
Let me repeat that I strongly recommend a more careful study of the available documentation!
That said, here is a sample extension that retrieves the DOM content on StackOverflow pages and sends it to the background page, which in turn prints it in the console:
background.js:
// Regex-pattern to check URLs against.
// It matches URLs like: http[s]://[...]stackoverflow.com[...]
var urlRegex = /^https?:\/\/(?:[^./?#]+\.)?stackoverflow\.com/;
// A function to use as callback
function doStuffWithDom(domContent) {
console.log('I received the following DOM content:\n' + domContent);
}
// When the browser-action button is clicked...
chrome.browserAction.onClicked.addListener(function (tab) {
// ...check the URL of the active tab against our pattern and...
if (urlRegex.test(tab.url)) {
// ...if it matches, send a message specifying a callback too
chrome.tabs.sendMessage(tab.id, {text: 'report_back'}, doStuffWithDom);
}
});
content.js:
// Listen for messages
chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
// If the received message has the expected format...
if (msg.text === 'report_back') {
// Call the specified callback, passing
// the web-page's DOM content as argument
sendResponse(document.all[0].outerHTML);
}
});
manifest.json:
{
"manifest_version": 2,
"name": "Test Extension",
"version": "0.0",
...
"background": {
"persistent": false,
"scripts": ["background.js"]
},
"content_scripts": [{
"matches": ["*://*.stackoverflow.com/*"],
"js": ["content.js"]
}],
"browser_action": {
"default_title": "Test Extension"
},
"permissions": ["activeTab"]
}
Update for manifest v3
chrome.tabs.executeScript doesn't work in manifest v3, as noted in the comments of this answer. Instead, use chrome.scripting. You can specify a separate script to run instead of a function, or specify a function (without having to stringify it!).
Remember that your manifest.json will need to include
...
"manifest_version": 3,
"permissions": ["scripting"],
...
You don't have to use the message passing to obtain or modify DOM. I used chrome.tabs.executeScriptinstead. In my example I am using only activeTab permission, therefore the script is executed only on the active tab.
part of manifest.json
"browser_action": {
"default_title": "Test",
"default_popup": "index.html"
},
"permissions": [
"activeTab",
"<all_urls>"
]
index.html
<!DOCTYPE html>
<html>
<head></head>
<body>
<button id="test">TEST!</button>
<script src="test.js"></script>
</body>
</html>
test.js
document.getElementById("test").addEventListener('click', () => {
console.log("Popup DOM fully loaded and parsed");
function modifyDOM() {
//You can play with your DOM here or check URL against your regex
console.log('Tab script:');
console.log(document.body);
return document.body.innerHTML;
}
//We have permission to access the activeTab, so we can call chrome.tabs.executeScript:
chrome.tabs.executeScript({
code: '(' + modifyDOM + ')();' //argument here is a string but function.toString() returns function's code
}, (results) => {
//Here we have just the innerHTML and not DOM structure
console.log('Popup script:')
console.log(results[0]);
});
});
For those who tried gkalpak answer and it did not work,
be aware that chrome will add the content script to a needed page only when your extension enabled during chrome launch and also a good idea restart browser after making these changes

Get HTML content from tabs

I'm totally at a loss here. I want to get html content from tabs in Chrome.
manifest.json
{
"manifest_version": 2,
"name": "Test",is a test.",
"version": "1.0",
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"background": {
"scripts": ["main.js"],
"persistent": false
},
"permissions": [
"tabs",
"https://www.google.com"
]
}
main.js
var timerObj = new Timer({'interval':5000});
chrome.runtime.onStartup.addListener(timerObj.start(mainF));
function mainF() {
chrome.tabs.query( {} ,function (tabs) {
for (var i = 0; i < tabs.length; i++) {
var url = tabs[i].url;
if (url != null) {
console.log(tabs[i].url);
//I want to get html source here
}
}
});
};
function Timer( obj ){
The last line function Timer( obj ){ is truncated for brevity. console.log(tabs[i].url); is there for testing. For each tab, I wish to get the html source. With that source, I'll parse for tags and other content. I've seen other resources mentioning sendMessage and onMessage, but I'm not really getting it. Many other resources refer to the deprecated sendRequest and onRequest.
To my knowledge, there are three ways to implement this.
chrome.tabs.executeScript. We can use Programming Injection to inject content script into web page, in the callback we can get the returned value.
Content Script and Message Passing. We can also inject content script in a way of manifest.json, then use chrome.runtime.sendMessage and chrome.runtime.onMessage to transfer the data.
XMLHttpRequest. Yes, this is also a way, we can directly make an ajax call in background page to get the source code of web page, because we could easily get the url. Obviously we need to start another http request compared with above two methods, but this is also a way.
In below sample code, I just use browserAction to trigger the event, then you can switch different methods to get the source code of web page by commenting out other two methods and reserve only one.
manifest.json
{
"manifest_version": 2,
"name": "Test is a test.",
"version": "1.0",
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"content.js"
]
}
],
"background": {
"scripts": [
"background.js"
],
"persistent": false
},
"browser_action": {
"default_title": "title"
},
"permissions": [
"tabs",
"<all_urls>"
]
}
background.js
chrome.browserAction.onClicked.addListener(function () {
chrome.tabs.query({}, function (tabs) {
for (var i = 0; i < tabs.length; i++) {
var id = tabs[i].id;
var url = tabs[i].url;
//method1(id);
method2(id);
//method3(url);
}
});
});
function method1(tabId) {
chrome.tabs.executeScript(tabId, { "code": "document.documentElement.outerHTML;" }, function (result) {
console.log(result);
});
}
function method2(id) {
chrome.tabs.sendMessage(id, {action: "getSource"}, function(response) {
console.log(response.sourceCode);
});
}
function method3(url) {
var xhr = new XMLHttpRequest();
xhr.onload = function () {
console.log(xhr.responseText);
};
xhr.open("GET", url);
xhr.send();
}
content.js
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if(request.action === "getSource") {
sendResponse({sourceCode: document.documentElement.outerHTML});
}
});

Google Extension: Access to DOM in iframe of a different domain

So I've found several pages on here, as well as various blog posts that seem to do pretty much exactly what I want to do, but they are all a few years old and seem really easy but don't work.
As the title says, On thisdomain.com there is a iframe from thatdomain.com and I want to get the value in a div in that iframe.
Manifest.json
{
"manifest_version": 1,
"name": "MyExtention",
"version": "1.0",
"description": "Nothing Yet",
"permissions": [
"storage",
"tabs",
"unlimitedStorage",
"webRequest",
"webNavigation",
"*://*.match-both-iframe-and-main-domain.com/*",
"*://*/*"
],
"background": {
"scripts": ["listener.js"],
"persistent": true
},
"content_scripts":
[
{
"matches": ["*://*.matchnothing.shshdjdjffkdj.com/*"],
"js": ["mainscript.js"],
"all_frames": true
}
]
}
The content script url matches nothing because it is fired from a listener (which works). Basically it waits for a request from one of 2 urls before it activates.
listener.js
var chrome = chrome || {};
var callback = function(listenerRes) {
console.log(listenerRes.url);
if (listenerRes.url.indexOf("listenurl1") > -1 ||
listenerRes.url.indexOf("listenurl2") > -1) {
chrome.tabs.get(listenerRes.tabId, function(tab) {
chrome.tabs.executeScript(tab.id, {file: "mainscript.js"});
});
}
};
chrome.webRequest.onBeforeRequest.addListener( callback, {urls: ["*://*.google.com/*"]} );
mainscript.js
var chrome = chrome || {};
... // helper functions and such
var iframe = document.getElementsByid('myiframe');
// Get all data.
var datas = [];
try {
datas = iframe.contentWindow.document.getElementsByClassName('mydata'); // error is here
}catch(e){
console.log(e);
}
... // do stuff with the data
On the commented line it throws a "Blocked a frame with origin URL from accessing a cross-origin frame."
So I am under the impression that some combination of all_frames = true, the chrome.tabs.executeScript, and the domains in the permissions should allow for this to work. But it doesn't.
It might be important to note, the reason for this listener is because the iframe isnt on the page to start.
Please help, Im an experienced web developer but this is my 1st foray into Chrome Extentions.

Trying to submit my first chrome extension to the store for testing but receiving this error upon upload

An error occurred: The package is using features that are no longer permitted in new applications. Please refer to the blog post for details.
I will be hosting an image of Wikionaty elsewhere for the finished product, just testing at the moment.
From the link above: "Beginning this week, you won’t be able to publish legacy packaged apps in the Chrome Web Store that request any of the following permissions:
(a) any host permissions, including "< all urls >" ". Is this my problem? How can I get around this? There are many extensions that use a dictionary.
This is my manifest.json:
{
"name": "my app",
"description": "this is my app",
"version": "1.4",
"manifest_version": 2,
"content_security_policy": "script-src 'self' https://en.wiktionary.org; object-src 'self'",
"background": {
"page": "background.html"
},
"app": {
"launch": {
"local_path": "index.html"
}
},
"icons": {
"128": "icon.png",
"16": "icon.png"
},
"permissions": [
"http://*/*",
"https://*/*",
"https://en.wiktionary.org/",
"tabs",
"contextMenus",
"storage",
"unlimitedStorage",
"notifications"]
}
The offending JS. The extension was uploading before this was added
var baseURL = 'http://en.wiktionary.org';
function showPage(page,text) {
var sourceurl = baseURL + '/wiki/' + page;
$('#pagetitle').text(page);
$('#wikiInfo').html(text);
$('#sourceurl').attr('href',sourceurl);
$('#licenseinfo').show();
$('#wikiInfo').children("ol:lt(1)").attr('',
function() { //console.log(this);
$(" ol li ul").detach();
$(" ol li ul").detach();
var wikiDefine = this.textContent;
var wikiDefineShort = jQuery.trim(wikiDefine).substring(0, 500) the definition for google local storage
.trim(this) + "...";
runArray();
wordObject[wordObject.length]= { word: page, definition: wikiDefineShort };
runArray();
chrome.storage.sync.set({"myValue": wordObject}); /////save
});
}
$(document).ready(function() {
$('#pagetitle').hide();
$('#word').change(function() {
var page = this.value.toLowerCase();
$('#loading').html('...please wait...');
$.getJSON(baseURL+'/w/api.php?action=parse&format=json&prop=text|revid|displaytitle&page='+page,
function(json) {
$('#loading').html('');
console.log(json.parse);
if(json.parse === undefined) {
console.log("word not found");
wordObject[wordObject.length]= { word: page, definition: "word not found - double click here to add definition" };
runArray();
chrome.storage.sync.set({"myValue": wordObject}); /////save
document.getElementById("word").value = "";
} else {
showPage(page,json.parse.text['*']);
$('#wikiInfo').html("<div></div>");
document.getElementById("word").value = "";
}
});
});
});

chrome.tabs.executeScript: How to get access to variable from content script in background script?

How to get access to variable app from content script app.js in background script background.js?
Here is how I try it (background.js):
chrome.tabs.executeScript(null, { file: "app.js" }, function() {
app.getSettings('authorizeInProgress'); //...
});
Here is what I get:
Here is manifest.json:
{
"name": "ctrl-vk",
"version": "0.1.3",
"manifest_version": 2,
"description": "Chrome extension for ctrl+v insertion of images to vk.com",
"content_scripts": [{
"matches": [
"http://*/*",
"https://*/*"
],
"js": ["jquery-1.9.1.min.js"
],
"run_at": "document_end"
}],
"web_accessible_resources": [
"jquery-1.9.1.min.js"
],
"permissions" : [
"tabs",
"http://*/*",
"https://*/*"
],
"background": {
"persistent": false,
"scripts": ["background.js"]
}
}
Full code for instance, at github
https://github.com/MaxLord/ctrl-vk/tree/with_bug
To avoid above error use following code
if (tab.url.indexOf("chrome-devtools://") == -1) {
chrome.tabs.executeScript(tabId, {
file: "app.js"
}, function () {
if (app.getSettings('authorizeInProgress')) {
alert('my tab');
REDIRECT_URI = app.getSettings('REDIRECT_URI');
if (tab.url.indexOf(REDIRECT_URI + "#access_token") >= 0) {
app.setSettings('authorize_in_progress', false);
chrome.tabs.remove(tabId);
return app.finishAuthorize(tab.url);
}
} else {
alert('not my');
}
});
}
instead of
chrome.tabs.executeScript(null, {
file: "app.js"
}, function () {
if (app.getSettings('authorizeInProgress')) {
alert('my tab');
REDIRECT_URI = app.getSettings('REDIRECT_URI');
if (tab.url.indexOf(REDIRECT_URI + "#access_token") >= 0) {
app.setSettings('authorize_in_progress', false);
chrome.tabs.remove(tabId);
return app.finishAuthorize(tab.url);
}
} else {
alert('not my');
}
});
Explanation
chrome://extensions/ page also fires chrome.tabs.onUpdated event, to avoid it we have to add a filter to skip all dev-tool pages.
(Would've submitted this as comment to the accepted answer but still lack the required reputation)
You should also give the tabId to chrome.tabs.executeScript as first argument when you have it. Otherwise you risk user switching windows/tabs right after requesting a URL and background.js doing executeScript against wrong page.
While fairly obvious on hindsight it threw me for a loop when I got that same error message "Cannot access contents of url "chrome-devtools://.." even though my chrome.tabs.onUpdated eventhandler was checking that the page user requested had some specific domain name just before doing the executeScript call.
So keep in mind, chrome.tabs.executeScript(null,..) runs the script in active window, even if the active window might be developer tools inspector.
We should notice that, in the manifest cofig:
"content_scripts": [{
"matches": [
"http://*/*",
"https://*/*"
],
"js": ["jquery-1.9.1.min.js"
],
in the "matches" part, only http, https are matched, so if you load your extension in page like: 'chrome://extensions/', or 'file:///D:xxx', that error will occur.
You may load your extension in the page with the url 'http://'; or add more rules in your 'matches' array.

Categories