Dynamically inject content scripts - Chrome Extension - javascript

I'm trying to make a chrome extension that receives javascript code from a backend and saves it in localStorage (as base64) so I can later inject it as a content script when the right page is loaded, it does work most of the time except there are a few issues... First issue (not that important) is that I cannot access Chrome APIs (like chrome.storage or chrome.runtime.sendMessage), second issue is that it doesn't inject the correct code to child iframes... because location.href returns the URL of the top webpage and I couldn't find a way to access current URL of iframe within the iframe itself.
This is my code so far:
manifest.json
//....
"content_scripts": [{
"run_at": "document_end",
"all_frames": true,
"matches": [
"<all_urls>"
],
"js": [
"src/inject/InjectManager.js"
]
}],
//...
InjectManager.js:
// Some functions were not included for brevity
chrome.runtime.sendMessage({ action: "get_supported_urls" }, function(supported_urls) {
let current_url = window.location.href;
// Check if we support current_url
let js_code_to_inject = isWebsiteSupported(supported_urls, current_url); // this function returns string that is javascript code.
if(js_code_to_inject){
// Append the code to the body
let script = document.createElement("script");
script.type = "text/javascript";
script.innerHTML = js_code_to_inject;
document.body.appendChild(script);
}
});
As you see, I'm kind of trying to recreate what chrome already does in manifest.json's "content_script" section because my javascript code is dynamic.
Note: I know this is not allowed on chrome store and such, this extension is not to be shared with anyone.
Thanks for reading.
Any help would be greatly appreciated.

I cannot access Chrome APIs (like chrome.storage or chrome.runtime.sendMessage)
Your code currently makes a page script, not a content script. For the latter you need to use chrome.tabs.executeScript (see also content script documentation) in the background script.
location.href returns the URL of the top webpage and I couldn't find a way to access current URL of iframe within the iframe itself.
No, what you're describing simply cannot happen, it would be a doomsday-level violation of URL origin security, so something else is happening. For example, your manifest.json doesn't have match_about_blank meaning InjectManager.js doesn't process the dynamically added about:blank frames at all.
manifest.json:
"content_scripts": [{
"match_about_blank": true,
.....................
InjectManager.js:
chrome.runtime.sendMessage({ action: 'inject', data: location.href });
background script:
chrome.runtime.onMessage.addListener(({ action, data }, sender, sendResponse) => {
if (action === 'inject') {
chrome.tabs.executeScript(sender.tab.id, {
code: getInjectionCode(data),
frameId: sender.frameId,
});
}
});
Note that some iframes like javascript: or srcdoc: won't run content scripts in Chrome at all so you'll have to process them directly in InjectManager.js as they cannot be injected by executeScript(). For example you can find all iframes by using document.querySelectorAll('iframe') and create DOM script element inside like you do currently but you'll use frameElement.contentDocument instead of document and of course you'll check whether the iframe's real URL (frameElement.contentWindow.location.href) doesn't start with http because frames can be navigated inside without changing their src attribute on the outside. Do the check inside try/catch because accessing an iframe from a different origin will throw.

Related

Chrome extensions: chrome.storage.local.get headache

Trying to build a simple Chrome extension to output the whole contents of localStorage.
How difficult should it be? Spent a few hours researching and trying, still doesn't work :(
Appreciate assistance!
For example:
function load() {
chrome.storage.local.get(null, function(items) {
var allKeys = Object.keys(items);
alert(allKeys.count);
});
}
document.addEventListener('DOMContentLoaded', () => {
load();
});
outputs 'undefined'
Some other ways I tried actually contained an object, but I couldn't do anything with it (no count/length, no JSON.stringify, no toString() etc).
Manifest:
"permissions": [
"activeTab",
"storage",
"tabs",
"http://*/*",
"https://*/*",
"chrome://favicon/"
]
How can I output the whole localStorage within my extension popup?
Please help!
Could you try allKeys.length?
Object.keys(items) would return you an array. Array by default doesn't have a property count. I think you mean length.
If you need the webpage's local storage (the domain loaded in your tab), instead of the extension page's local storage, I don't think you can do that.
Iván Nokonoko had a nice point that you need to inject the script in order to interact with the loaded web page, however the problem is the injected script is forced to run in an isolated environment, in which you only have access to the DOM tree, but no access to the original JS environment, thus the localStorage variable that you can get, is the one in your own isolated env, rather than the original one.
Quoted from Chrome Content Scripts API:
However, content scripts have some limitations. They cannot:
Use variables or functions defined by web pages or by other content scripts
And I believe injecting code has the same restrication as content script.
I think this restriction is for safety consideration. It's OK to expose the DOM tree to extensions, in the worst case the extension would mess up the view. But if expose the JS env, such as the local storage to extension, then it could be dangerous, as extension is capable of accessing/tampering data.
To show the localStorage of currently active tab, try:
chrome.tabs.executeScript({code:'JSON.stringify(localStorage)'},function(result) {
console.log(JSON.parse(result[0]));
});
Working example:
popup.html
<body>
<div id=ls></div>
<script src="popup.js"></script>
</body>
popup.js
chrome.tabs.executeScript({code:'JSON.stringify(localStorage)'}, res => {
var ls = document.querySelector("#ls"), lsObj = JSON.parse(res[0]), lsHtml = "";
for (var key in lsObj)
lsHtml += "<p>" + key + " : " + lsObj[key] + "</p>";
ls.innerHTML = lsHtml ? lsHtml : "The active tab has nothing in localStorage";
});

opening window and navigating through page

I am trying to navigate a webpage with a content script. However the function keeps running because every time the page changes it runs through the script. I was wondering if there was a better way to do this as it is a resource hog and also disallows the user to interact with the page because of the constant refreshing.
Here's the code in question, sorry if it looks weird. I do not have more then a few weeks of knowledge in jQuery.
$(document).ready(function(){
console.log("Made it here" + window.location.href);
loc = window.location.href;
match = loc.match('http://url.com/uc/');
if (match) {
window.location = "http://url.com/uc/health/index/1";
console.log("2 here");
window.location = "http://url.com/uc/health/communication/createSelectTemplate";
console.log("3 here");
chrome.storage.local.get('formOption', function(result) {
document.getElementById('formTemplate').value = result.formOption;
document.forms[0].submit();
});
}
});
The reason i have to navigate three windows before using values because whoever made this website has timeout cookies and the pages can not be called before the previous one loads.
It is a content script tho, so all the code is on the next page. Maybe if there was a way to check exact url? But when i was playing with that awhile back the computer didn't distinguish between.
urlhere.com/uc/
and
urlhere.com/uc/health/index/1
Every time you navigate (e.g. immediately after assigning window.location), your script stops executing is unloaded with the page, and when the next page loads, the content scripts are indeed loaded again. If the same script is loaded, with the same initial state, it will of course execute the same operation.
Possible solutions (there are many):
Be more precise with your matching (= better notice the actually changing state).
loc.match('http://url.com/uc/') will only check that the address includes that string - which all URLs you show do. Why not just use loc == 'http://url.com/uc/' (and check for intermediate pages)?
Use fine-grained content scripts (= load different scripts).
Manifest defines which pages get what scripts on load. I assume you have something like this:
"content_scripts" : [{
"js" : ["jquery.js", "content1.js"],
"matches": ["http://*"]
}]
You can make several scripts and let Chrome parse the URLs. For example, content1.js will do the first redirect, content2.js will do the second.
"content_scripts" : [{
"js" : ["jquery.js", "content1.js"],
"matches": ["http://url.com/uc/"]
}, {
"js" : ["jquery.js", "content2.js"],
"matches": ["http://url.com/uc/health/index/1"]
}]
Use some persistent state (that persists between navigation) to indicate which stage of redirect are you on (= control changing state yourself).
The page's sessionStorage is ideal for this, as it's only persistent within the tab:
if (match) {
switch (sessionStorage.redirectStage) {
case 3:
// We're at the final page, do actual work
break;
case 2:
sessionStorage.redirectStage = 3;
window.location = "http://url.com/uc/health/communication/createSelectTemplate";
break;
default: // Includes initial state when it's unset
window.location = "http://url.com/uc/health/index/1";
}
}

How to inject page scipt to main document and execute it as first script using chrome-extension

I'm working on chrome-extension, I need to inject page script to main document. The script has to be executed as first script.
In FF extension there is an option to change response body, so I'm looking for responses with html documents and I change the response from <html><head>... to <html><script src='pagescript'></script><head>...
Looks like in chrome-extension I'm not able to change response body so I'm trying different approach.
In manifest.json I defined content script that should be executed on document_start. Also I added my pagescript.js to web_accessible_resources
"web_accessible_resources": [
"pageScript.js"
],
"content_scripts": [{
"matches": [
"<all_urls>"
],
"js": [
"src/add-page-scripts.js"
],
"all_frames": true,
"run_at": "document_start"
}]
In content-script I tried different ways to inject my page script to document
1)
var script = document.createElement('script');
script.src = chrome.extension.getURL("pageScript.js");
document.documentElement.appendChild(script);
In this case PageScript.js is the first element in html, but it's executed asynchronously, so when it's executed, other html elements may already exists or even other scripts may be executed
2)
var script = document.createElement('script');
script.textContent = "alert('Page script')";
document.documentElement.appendChild(script);
In this case cript is executed as first script, so this is nearly what I need. But I would have to have pagescript.js content instead of url
3) I tried to extend #2 and inject script that will inject my pagescript using document.write.
var script = document.createElement('script');
script.textContent = "document.write(\"<script>alert('Page script')<\"+ \"/script>\")";
document.documentElement.appendChild(script);
But once I use document.write, the original page is not loaded at all. Looks like document.write breaks page loading. It's weird because my script is first script on the page, document.readyState returns loading, so I guess document.write should be safe (but it's not)
Questions are:
why document.write breaks page?
is there other way to inject page script to document and make sure it's executed as first script in document (even before other html elements are created?

chrome.tabs.executeScript and injection only into pages that pass matches filter in manifest.json

I'm attempting to perform programmatic injection of my content script into open tabs after my Chrome extension is reloaded or updated.
My script may call the following method for an arbitrary tab:
var manifest = chrome.app.getDetails();
var scripts = manifest.content_scripts[0].js;
chrome.tabs.executeScript(nTabID, {
file: scripts[0]
});
This works, except when I try to load it into a page that was not supposed to have a content script running according to the matches clause in the manifest.json. I get the following exception:
Cannot access contents of url "actual-url-here". Extension manifest
must request permission to access this host.
So my question. Is there a way to parse the page URL and see if it matches matches clause from manifest.json and prevent calling chrome.tabs.executeScript for unnecessary URL?
PS. I understand that one "hacky" solution is to catch-and-ignore exceptions. So I'm not asking for it.
When you use chrome.tabs.query for a list of tabs, use the url attribute to filter by a match patterns. As of Chrome 39, this key also supports an array of match patterns. If you need to support Chrome 38 or earlier, or if you got the tabs without chrome.tabs.query, use the parse_match_pattern function from this answer to filter tabs. To use it, copy that function and include it within your (background) page (e.g. by pasting it before the following snippet).
var content_scripts = chrome.runtime.getManifest().content_scripts;
// Exclude CSS files - CSS is automatically inserted.
content_scripts = content_scripts.filter(function(content_script) {
return content_script.js && content_script.js.length > 0;
});
content_scripts.forEach(function(content_script) {
try {
// NOTE: an array of patterns is only supported in Chrome 39+
chrome.tabs.query({
url: content_script.matches
}, injectScripts);
} catch (e) {
// NOTE: This requires the "tabs" permission!
chrome.tabs.query({
}, function(tabs) {
var parsed = content_script.matches.map(parse_match_pattern);
var pattern = new RegExp(parsed.join('|'));
tabs = tabs.filter(function(tab) {
return pattern.test(tab.url);
});
injectScripts(tabs);
});
}
function injectScripts(tabs) {
tabs.forEach(function(tab) {
content_script.js.forEach(function(js) {
chrome.tabs.executeScript(tab.id, {
file: js
});
});
});
}
});
The previous snippet inserts a content script in all tabs. It is your responsibility to make sure that inserting the script does not conflict with an earlier/later instance of your script.
Mimicking the all_frames and match_about_blank functionality is slightly more complex, because the chrome.tabs.executeScript API cannot be used to target specific frames (crbug.com/63979). If you want to inject in frames as well, then you have to insert in every tab (because there might be a frame under the non-matching top-level frame that matches the URL) and check the page's URL within the content script.
Finally, note that your content script must also deal with the fact that it may run at a point different from "run_at". In particular, content scripts that rely on "run_at":"document_start" might fail to work because calling chrome.tabs.executeScript will cause a script to be injected far past the document_start phase.

Chrome Extension: How to redirect to a custom HTML page in response to specific web request?

I'd like to write an extension that redirects all web traffic to a specific domain, let's say wikipedia.org, to an intermediate page that says something like, "Whoa, bub. You're about to go to Wikipedia. Are you sure about this?" followed by Yes and No buttons.
How do I respond to a specific URL request and replace the desired page with a custom one when it meets the condition of being a page with "wikipedia.org" in the domain?
You can do this using webRequest feature, a background page, and custom page with yes and no buttons. For example, write something similar in the background page:
var URLStorage;
function interceptRequest(request)
{
if(request && request.url)
{
if(request.type == "main_frame") // new page/site is loading in main window
{
if(request.url.indexOf("wikipedia.org") > -1)
{
URLStorage = request.url;
return {redirectUrl: chrome.extension.getURL("confirmation.html")};
}
}
}
}
chrome.webRequest.onBeforeRequest.addListener(interceptRequest, {urls: ["*://*/*"]}, ['blocking']);
This example does not strictly check if wikipedia is mentioned in domain, but I did this for clarity. In my real code a special class 'URL' is used which parses passed url and provides properties for every part of it, so they can be checked selectively.
In the confirmation.html just place 2 buttons, and bind them to an appropriate code, for example redirecting to requested site, if a user answered "yes".
$('#okbutton').click(function()
{
document.location.href = chrome.extension.getBackgroundPage().URLStorage;
});
Don't forget to mention "webRequest" and "webRequestBlocking" in permissions section of your manifest.
You can create a content script that injects javascript code into each page that the user visits. In your content script you could have the js check the current url against invalid url's and redirect them accordingly.
I think content scripts load after the page has loaded so there may be a brief period where the user sees the page they were looking for and then gets redirected to your landing page. Check out the content script docs here:
http://developer.chrome.com/extensions/content_scripts.html
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["http://www.google.com/*"],
"css": ["mystyles.css"],
"js": ["jquery.js", "myscript.js"]
}
],
...
}
"matches" you should make the array of something similar to
"matches": ["http://www.*.com/*", "http://*.com/*, "https://www.*.com/*", "https://*.*.com/*]
and "js" would be the name of your javascript file that you want to use to write the injection into the page.
something like:
if(window.location == "http://wikipedia.com"){
window.location.href = "http://mysplashpage.com";
}
Of course, that js won't work in all instances, for instance, if the user is trying to get to a directory of the target website. You will probably need to some regex checks or some other functions like protocol and host as defined here : http://css-tricks.com/snippets/javascript/get-url-and-url-parts-in-javascript/

Categories