How can I replace my webRequest blocking with declarativeNetRequest? - google-chrome-extension

I'm try to migrate my chrome extension to Manifest V3, and having some troubles with webRequest. I don't know how to replace the below source code to declarativeNetRequest. Also, what should I add to my manifest.json to make this work. Because when I read on the Chrome Developers, it mentions some thing about rules, and I don't know what those rules are. Thank you so much for your help
chrome.webRequest.onHeadersReceived.addListener(info => {
const headers = info.responseHeaders; // original headers
for (let i=headers.length-1; i>=0; --i) {
let header = headers[i].name.toLowerCase();
if (header === "x-frame-options" || header === "frame-options") {
headers.splice(i, 1); // Remove the header
}
if (header === "content-security-policy") { // csp header is found
// modifying frame-ancestors; this implies that the directive is already present
headers[i].value = headers[i].value.replace("frame-ancestors", "frame-ancestors " + window.location.href);
}
}
// Something is messed up still, trying to bypass CORS when getting the largest GIF on some pages
headers.push({
name: 'Access-Control-Allow-Origin',
value: window.location.href
})
// return modified headers
return {responseHeaders: headers};
}, {
urls: [ "<all_urls>" ], // match all pages
types: [ "sub_frame" ] // for framing only
}, ["blocking", "responseHeaders"]);
IMAGE OF THE SOURCE CODE

Related

Make a chrome extension download all PDFs with `declarativeNetRequest`

I have to migrate a chrome extension from MV2 to MV3, and that means replacing usages of the blocking chrome.webRequest API with declarativeNetRequest. One usage is this:
function enableDownloadPDFListener() {
chrome.webRequest.onHeadersReceived.addListener(downloadPDFListener);
}
function downloadPDFListener(details) {
const header = details.responseHeaders.find(e => e.name.toLowerCase() === 'content-type');
if (header.value && header.value === 'application/pdf') {
const headerDisposition = details.responseHeaders.find(
e => e.name.toLowerCase() === 'content-disposition'
);
if (headerDisposition) {
headerDisposition.value = headerDisposition.value.replace('inline', 'attachment');
} else {
details.responseHeaders.push({ name: 'Content-Disposition', value: 'attachment' });
}
}
return { responseHeaders: details.responseHeaders };
}
Explanation: This function intercepts requests, checks if their Content-Type header is application/pdf, and if that's the case, sets Content-Disposition: attachment to force downloading the file. We have this functionality to save our employees time when downloading lots of PDF files from various websites.
The problem I'm facing is that this API is deprecated and can't be used in Manifest V3, and I wasn't able to migrate it to the declarativeNetRequest API. I tried the following:
[
{
"id": 1,
"priority": 1,
"action": {
"type": "modifyHeaders",
"responseHeaders": [
{
"header": "content-disposition",
"operation": "set",
"value": "attachment"
}
]
},
"condition": {
// what should I put here?
}
}
]
But I don't know how to filter files with a certain Content-Type header. From what I understand, this is currently not possible. Is there any other way to get this functionality in Chrome's MV3?
I tried { "urlFilter": "*.pdf" } as a condition, which isn't correct, but might be good enough. However, although the badge indicates that the rule was executed, the Content-Disposition header isn't set in the network tab, and the file isn't downloaded. What went wrong here?
At a glance, I don't think there's a condition that would work here. It seems like a reasonable use case and something that may make a good issue at https://bugs.chromium.org/p/chromium though.
In the meantime - could you have an extension which injects a content script that listens to the click event on links? Or alternatively you could perhaps wait for the PDF to open and then close the tab and perform a download.

Native way of making chrome extension active only in specific URLs

As you can see from the code below, I'm checking the contents of manifest.json to get the matches and exclude_matches in my content_scripts option. I think it works as intended, because I'm seeing the action active only in the URLs listed in "matches" property, not in the ones in "exclude_matches". However, this seems to be too error-prone, because I may have to modify the string replacements in regExpFromMatch if I need to update these match patterns in the future. Is this really making the extension inactive or is it just the action in the toolbar? And, in order to avoid the probable need for repetitive refactoring in the future, I'd like to know if there's a simpler, safer, way to achieve this.
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === "complete") {
const {
content_scripts: [{ matches, exclude_matches }],
} = chrome.runtime.getManifest();
const actionEnabled = isActionEnabledInThisURL(
[matches, exclude_matches],
tab.url
);
if (actionEnabled) {
chrome.action.enable(tabId);
} else {
chrome.action.disable(tabId);
}
// *************************************************************
function isActionEnabledInThisURL(contentMatchPatterns, url) {
const [isMatch, isExcludedMatch] = contentMatchPatterns.map(
(urlMatchPattern) =>
regExpFromMatch(urlMatchPattern).some((regExp) => regExp.test(url))
);
return isMatch && !isExcludedMatch;
}
function regExpFromMatch(matchArray) {
return matchArray.map(
(string) =>
new RegExp(string.replace(/\//g, `\/`).replace(/\*/g, ".*"), "g")
);
}
}
});

How to filter urls with # (hash tag) using UrlFilter in a chrome.declarativeContent.PageStateMatcher

I've just started building a chrome extension and as I need to display its icon only for specific urls, I used page_action.
I also used an event listening if the url changes and matches my pattern that way to display the icon:
chrome.declarativeContent.onPageChanged.addRules([
{
conditions: [
new chrome.declarativeContent.PageStateMatcher({
pageUrl: { urlContains: 'https://mysite.com/mypage.html' }
})
],
actions: [ new chrome.declarativeContent.ShowPageAction() ]
}
]);
It works fine but when I want to add a filter of the first character of the query, it fails.
The url pattern I want to filter looks like:
https://mysite.com/mypage.html#e123456789
I tried the following but it didn't help:
pageUrl: { urlContains: 'https://mysite.com/mypage.html#e' }
pageUrl: { urlContains: 'https://mysite.com/mypage.html', queryPrefix: '#e' }
pageUrl: { urlContains: 'https://mysite.com/mypage.html', queryPrefix: 'e' }
I think that the issue comes from the hash tag.
Any idea of a workaround ?
The #... part of a URL is called a "reference fragment" (ocassionally referred to as "hash").
Reference fragments are currently not supported in URLFilters, there is already a bug report for this feature: Issue 84024: targetUrlPatterns and URL search/hash component.
If you really want to show the page action depending on the state of the reference fragment, then you could use the chrome.webNavigation.onReferenceFragmentUpdated event instead of the declarativeContent API. For example (adapted from my answer to How to show Chrome Extension on certain domains?; see that answer for the manifest.json to use for testing):
function onWebNav(details) {
var refIndex = details.url.indexOf('#');
var ref = refIndex >= 0 ? details.url.slice(refIndex+1) : '';
if (ref.indexOf('e') == 0) { // Starts with e? show page action
chrome.pageAction.show(details.tabId);
} else {
chrome.pageAction.hide(details.tabId);
}
}
// Base filter
var filter = {
url: [{
hostEquals: 'example.com'
}]
};
chrome.webNavigation.onCommitted.addListener(onWebNav, filter);
chrome.webNavigation.onHistoryStateUpdated.addListener(onWebNav, filter);
chrome.webNavigation.onReferenceFragmentUpdated.addListener(onWebNav, filter);

Cache all images with onHeadersReceived

I'm trying to modify the response headers of the images to save bandwith and improve the response time.These are my files:
manifest.json
{
"name": "Cache all images",
"version": "1.0",
"description": "",
"background": {"scripts": ["cacheImgs.js"]},
"permissions": [ "<all_urls>", "webRequest", "webRequestBlocking" ],
"icons": {"48": "48.png"},
"manifest_version": 2
}
cacheImgs.js
var expDate = new Date(Date.now()+1000*3600*24*365).toUTCString();
var newHeaders =
[{name : "Access-Control-Allow-Origin", value : "*"},
{name : "Cache-Control", value : "public, max-age=31536000"},
{name : "Expires", value : expDate},
{name : "Pragma", value : "cache"}];
function handler(details) {
var headers = details.responseHeaders;
for(var i in headers){
if(headers[i].name.toLowerCase()=='content-type' && headers[i].value.toLowerCase().match(/^image\//)){
for(var i in newHeaders) {
var didSet = false;
for(var j in headers) {
if(headers[j].name.toLowerCase() == newHeaders[i].name.toLowerCase() ) {
headers[j].value = newHeaders[i].value;
did_set = true; break;
}
}
if(!didSet) { headers.push( newHeaders[i] ); }
}
break;
}
}
console.log(headers);
return {responseHeaders: headers}
};
var requestFilter = {urls:['<all_urls>'], types: ['image'] };
var extraInfoSpec = ['blocking', 'responseHeaders'];
chrome.webRequest.onHeadersReceived.addListener(handler, requestFilter, extraInfoSpec);
the console.log fires many times and i can see the new headers. The problem is that when I open the chrome developer tools of the page, in the network tab, i see the same original headers of the images. Also note the blocking value in the extraInfoSpec, so that's supposed to be synchronous. Does someone happen the same?
UPDATE
Now I see the modified response headers in the network panel.
But now I only see from images whose initiator is the webpage itself. The images whose initiator are jquery.min.js doesn't change the response headers
There are two relevant issues here.
First, the headers displayed in the developer tools are those that are received from the server. Modifications by extensions do not show up (http://crbug.com/258064).
Second (this is actually more important!), modifying the cache headers (such as Cache-control) has no influence on the caching behavior of Chromium, because the caching directives have already been processed when the webRequest API is notified of the headers.
See http://crbug.com/355232 - "Setting caching headers in webRequest.onHeadersReceived does not influence caching"
After doing some research, it turns out my previous answer was wrong. This is actually a Chrome bug - Chrome's DevTools Network panel will only show the actual headers received from the server. However, the headers you've injected will still have the desired effect.
Another extension developer identified the issue here and provided a link to the Chrome defect report

Can I allow the extension user to choose matching domains?

Can I allow the domain matching for my extension to be user configurable?
I'd like to let my users choose when the extension runs.
To implement customizable "match patterns" for content scripts, the Content script need to be executed in by the background page using the chrome.tabs.executeScript method (after detecting a page load using the chrome.tabs.onUpdated event listener).
Because the match pattern check is not exposed in any API, you have to create the method yourself. It is implemented in url_pattern.cc, and the specification is available at match patterns.
Here's an example of a parser:
/**
* #param String input A match pattern
* #returns null if input is invalid
* #returns String to be passed to the RegExp constructor */
function parse_match_pattern(input) {
if (typeof input !== 'string') return null;
var match_pattern = '(?:^'
, regEscape = function(s) {return s.replace(/[[^$.|?*+(){}\\]/g, '\\$&');}
, result = /^(\*|https?|file|ftp|chrome-extension):\/\//.exec(input);
// Parse scheme
if (!result) return null;
input = input.substr(result[0].length);
match_pattern += result[1] === '*' ? 'https?://' : result[1] + '://';
// Parse host if scheme is not `file`
if (result[1] !== 'file') {
if (!(result = /^(?:\*|(\*\.)?([^\/*]+))(?=\/)/.exec(input))) return null;
input = input.substr(result[0].length);
if (result[0] === '*') { // host is '*'
match_pattern += '[^/]+';
} else {
if (result[1]) { // Subdomain wildcard exists
match_pattern += '(?:[^/]+\\.)?';
}
// Append host (escape special regex characters)
match_pattern += regEscape(result[2]);
}
}
// Add remainder (path)
match_pattern += input.split('*').map(regEscape).join('.*');
match_pattern += '$)';
return match_pattern;
}
Example: Run content script on pages which match the pattern
In the example below, the array is hard-coded. In practice, you would store the match patterns in an array using localStorage or chrome.storage.
// Example: Parse a list of match patterns:
var patterns = ['*://*/*', '*exampleofinvalid*', 'file://*'];
// Parse list and filter(exclude) invalid match patterns
var parsed = patterns.map(parse_match_pattern)
.filter(function(pattern){return pattern !== null});
// Create pattern for validation:
var pattern = new RegExp(parsed.join('|'));
// Example of filtering:
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
if (changeInfo.status === 'complete') {
var url = tab.url.split('#')[0]; // Exclude URL fragments
if (pattern.test(url)) {
chrome.tabs.executeScript(tabId, {
file: 'contentscript.js'
// or: code: '<JavaScript code here>'
// Other valid options: allFrames, runAt
});
}
}
});
To get this to work, you need to request the following permissions in the manifest file:
"tabs" - To enable the necessary tabs API.
"<all_urls>" - To be able to use chrome.tabs.executeScript to execute a content script in a specific page.
A fixed list of permissions
If the set of match patterns is fixed (ie. the user cannot define new ones, only toggle patterns), "<all_urls>" can be replaced with this set of permissions. You may even use optional permissions to reduce the initial number of requested permissions (clearly explained in the documentation of chrome.permissions).

Resources