I'm trying to write my first chrome extension for a specific website, automatically downloading streams that are loaded by the site. I've come as far as downloading the stream using the chrome.debugger api, but now i want to auto name the stream using data on the website.
First, let me give you the relevant scripts:
manifest.json
---------------------------------------
{
"name": "Downloader script",
"description": "Downloads stuff",
"version": "0.1",
"permissions": [
"debugger",
"downloads"
],
"background": {
"scripts": ["background.js"]
},
"browser_action": {
"default_icon": "icon.png",
"default_title": "Downloader"
},
"manifest_version": 2
}
here is the background.js, it displays a window stating all the files that have been downloaded.
background.js
--------------------------
chrome.browserAction.onClicked.addListener(function(tab) {
chrome.debugger.attach({tabId:tab.id}, version,
onAttach.bind(null, tab.id));
});
var version = "1.0";
function onAttach(tabId) {
if (chrome.runtime.lastError) {
alert(chrome.runtime.lastError.message);
return;
}
chrome.windows.create(
{url: "headers.html?" + tabId, type: "popup", width: 400, height: 600});
}
Here is the (very simple) headers.html that is called:
<html>
<head>
<style>
body {
font-family: monospace;
word-wrap: break-word;
}
#container {
white-space: pre;
}
.request {
border-top: 1px solid black;
margin-bottom: 10px;
}
</style>
<script src="headers.js"></script>
</head>
<body>
<div id="container"></div>
</body>
</html>
Now here comes the hard part, the headers.js
var tabId = parseInt(window.location.search.substring(1));
window.addEventListener("load", function() {
chrome.debugger.sendCommand({tabId:tabId}, "Network.enable");
chrome.debugger.onEvent.addListener(onEvent);
});
window.addEventListener("unload", function() {
chrome.debugger.detach({tabId:tabId});
});
var requests = {};
function onEvent(debuggeeId, message, params) {
if (tabId != debuggeeId.tabId)
return;
if (message == "Network.requestWillBeSent") {
var requestDiv = requests[params.requestId];
if (!requestDiv && params.redirectResponse) {
var requestDiv = document.createElement("div");
requestDiv.className = "request";
requests[params.requestId] = requestDiv;
downloadSong(params.requestId, params.redirectResponse);
}
document.getElementById("container").appendChild(requestDiv);
}
}
function downloadSong(requestId, response) {
var requestDiv = requests[requestId];
var newSong = document.createElement("div");
newSong.textContent = "New song is being downloaded...";
var songLink = document.createElement('a');
var songLinkText = document.createTextNode("Direct link to song");
songLink.appendChild(songLinkText);
songLink.title = "Right click -> save as...";
songLink.href = response.headers.location;
if (songLink.href.indexOf("//audio") > -1) {
requestDiv.appendChild(newSong);
requestDiv.appendChild(songLink);
chrome.downloads.download({url: response.headers.location},function(id){});
}
}
Now the website i'm taking the streams from has the following structure:
<div id="trackInfo" class="unselectable">
<div class="contents" style="display: block;">
<div class="info">
<div class="trackData">
<div>This is the song title i want to grab</div>
<div><i class="byText">by</i> Artist name i want to grab
</div>
<div><i class="onText">on</i> <a class="albumTitle" address="true" href="http://www.albumlink" style="">Album title i want to grab</a></div>
</div>
</div>
</div>
</div>
My question is: how can I grab the element data from the page (the DOM i believe it is called) and use it with the chrome.downloads api? Since the headers.js file is (i think) not in contact with the webpage itself, just the network part.
PS: My first time posting here, constructive criticism on my post would also be welcome.
I have a feeling you're hunting squirrel with a howitzer here, employing "debugger" to intercept network requests and inject DOM into a page.
For intercepting network requests, there's a dedicated API for that, chrome.webRequest.
You can attach a listener to chrome.webRequest.onBeforeRequest, and you will have the same kind of information as you are using in your chrome.debugger.onEvent listener.
For injecting your DIV and reading data from DOM, you (only) need a content script.
The following code will assume assume you'll also use jQuery (no reason not to, it's easier to manipulate DOM with it). And it's only skeleton code.
You can assign a content script to always load on the music service, and listen to messages from background with chrome.runtime.onMessage:
chrome.runtime.onMessage.addListener(function(message) {
if(message.songUrl){
var newSong = $('<div>').text(New song is being downloaded...);
/* fill the <div> and append it to the page */
}
});
When your background detects music stream download, it has tabId from the request event, and can message the required tab:
chrome.webRequest.onBeforeRequest.addListener(
function(details){
if (details.url.indexOf("//audio") > -1) {
chrome.tabs.sendMessage(tabId, {songUrl: details.url});
}
return {}; // Do not modify the request
},
/* filter details */
);
As for extracting song details, jQuery is your friend. You need to come up with a selector which will pick the element you need:
// Inside content script
var songName = $("#trackInfo .songTitle").text();
// etc.
Related
I am trying to invert the colors of PDF files viewed in the built-in PDF viewer from Firefox. I am able to achieve the desired effect by adding filter:invert(100%) within the .pdfViewer .page attribute, among others, in the Firefox style editor. However, when I try to apply this change via the extension I've created, no effect is imparted. I know the extension works on normal webpages and text files viewed in the browser.
manifest.json
{
"description": "blahblahblah",
"manifest_version": 2,
"name": "DarkWeb",
"version": "1.0",
"homepage_url": "blahblahblah",
"background": {
"scripts": ["background.js"]
},
"browser_action": {
"default_icon": "icons/sun.png",
"browser_style": true,
"default_popup":"popup/settings.html"
},
"permissions": [
"<all_urls>",
"tabs",
"activeTab"
],
"commands": {
"toggle-feature": {
"suggested_key": {
"default": "Ctrl+Space",
"mac": "Alt+Space"
}
}
}
}
background.js
//const CSS = "body {filter: invert(100%); background-color: white; color: black;}";
const TITLE_APPLY = "Apply CSS";
const TITLE_REMOVE = "Remove CSS";
const APPLICABLE_PROTOCOLS = ["http:", "https:"];
// Different settings for different buttons
var CSS = ""; // Will hold code for various filters
var previousID = ""; // Will hold previous button id for filters
const INVERT = ".pdfViewer .canvasWrapper {filter: invert(100%);}";
/*
Add an event listener
The popup window's event listener broadcasts a message, and this receives it
Upon receiving a message, it then runs updateFilter()
*/
browser.commands.onCommand.addListener(function(command) {
if (command == "toggle-feature") {
toggleFilters("Invert");
}
});
browser.runtime.onMessage.addListener(updateFilter);
function updateFilter(recieved, sender, sendResponse) {
toggleFilters(recieved.message);
sendResponse({response: "Response from background.js."});
}
// This listener is for newly-created tabs
// After the user switches to the new tab, the code then runs updateNewTab()
browser.tabs.onUpdated.addListener(updateNewTab);
function updateNewTab(recieved, sender, sendResponse) {
var tabID = browser.tabs.getCurrent().id;
browser.tabs.insertCSS(tabID, {code: CSS});
}
// Applies the desired filter's code to the CSS variable
function setCSScode(buttonID) {
switch(buttonID) {
case "Invert": CSS = INVERT; break;
default: break; // Do nothing for default
}
}
/*
Compares the current filter to the selected filter.
First removes the previous filter, and then checks if it should apply a new filter
If the selected filter is the same as the current filter, then it will just remove it.
Else, it will apply the new filter.
*/
function toggleFilters(buttonID) {
removeAllFilters(); // To apply a new filter, we must first remove the all filters
CSS = ""; // Reset the CSS variable. This fixes tab persistence.
if(previousID == buttonID) {
previousID = "";
}
else {
setCSScode(buttonID);
previousID = buttonID;
applyFilter();
}
}
// Apply the selected filter to all tabs
function applyFilter() {
var gettingAllTabs = browser.tabs.query({});
gettingAllTabs.then((tabs) => {
for (let currentTab of tabs) {
var tabID = currentTab.id;
browser.tabs.insertCSS(tabID, {code: CSS});
}
});
}
// Remove the all filters from all tabs
function removeAllFilters() {
var cssCodes = [INVERT];
var gettingAllTabs = browser.tabs.query({});
gettingAllTabs.then((tabs) => {
for (let currentTab of tabs) {
var tabID = currentTab.id;
cssCodes.forEach(function(item) {
var code = item; browser.tabs.removeCSS(tabID, {code: code});
});
}
});
}
I've removed a fair amount of impertinent code from background.js to hopefully aid in readability. Please forgive the sloppiness as I am still very new to this. Thanks!
EDIT: For clarification, I'm trying to accomplish this starting with the line const INVERT = ".pdfViewer .canvasWrapper {filter: invert(100%);}"; in background.js
It’s not possible anymore since extensions are no longer allowed to interact with the native pdf viewer.
https://bugzilla.mozilla.org/show_bug.cgi?id=1454760
I'm trying to change the attribute of the headline entries in any Google search by a Google Chrome extension.
By headline entries I mean these red-underlined:
Image: http://postimg.org/image/sgsyccbpf/
Looking at the HTML code of a random Google search with the Mozilla Firefox inspector:
Image: http://postimg.org/image/gsywhsmkj/
My idea was to obtain every element by looking for class name "rc". Maybe it's not a good idea, but I think it would work.
In order to develop the Chrome extension, I've written these files:
manifest.json
{
"name": "Test 1",
"version": "1.0.0",
"manifest_version": 2,
"content_scripts": [
{
"matches": [
"https://*/*",
"http://*/*",
"<all_urls>"
],
"js": ["content_scripts.js"],
"run_at": "document_start",
"all_frames": true
}
]
}
content:scripts.js
var doFilter = function() {
var classR = document.getElementsByClassName("rc");
for(var i=0; i < classR.length; i++) {
classR[i].setAttribute("style", "background-color:green");
classR[i].setAttribute("align", "center");
}
}
window.addEventListener("DOMContentLoaded", function() {
doFilter(document.body);
});
Here is a demonstration of how my extension worked in my own html page:
Image: postimg.org/image/bdi02zvfl (This is a link to a image but the system don't allow me to post more than two of them)
However, while searching normally in Google it does not work. Every "headline entry" should be green-backgrounded and centered as in the demonstration.
What am I missing?
Thanks!
Since the Google fetches the results asynchronously, you could use a MutationObserver to catche changes in the DOM and act accordingly. See this answer for a more detailed explanation and sample code.
Below is the code from the above question with a few modifications to achieve what you want. Editing the modifyElem() function, it should be easy to realize just about any modification.
content.js:
console.log("Injected...");
/* MutationObserver configuration data: Listen for "childList"
* mutations in the specified element and its descendants */
var config = {
childList: true,
subtree: true
};
/* Traverse 'rootNode' and its descendants and modify '.rc' elements */
function modifyElems(rootNode) {
var nodes = [].slice.call(rootNode.querySelectorAll('.rc'));
if (rootNode.className === 'rc') {
nodes.push(rootNode);
}
while (nodes.length > 0) {
var st = nodes.shift().style;
st.backgroundColor = 'green';
st.textAlign = 'center';
}
}
/* Observer1: Looks for 'div#search' */
var observer1 = new MutationObserver(function(mutations) {
/* For each MutationRecord in 'mutations'... */
mutations.some(function(mutation) {
/* ...if nodes have beed added... */
if (mutation.addedNodes && (mutation.addedNodes.length > 0)) {
/* ...look for 'div#search' */
var node = mutation.target.querySelector("div#search");
if (node) {
/* 'div#search' found; stop observer 1 and start observer 2 */
observer1.disconnect();
observer2.observe(node, config);
/* Modify any '.rc' elements already in the current node */
modifyElems(node);
return true;
}
}
});
});
/* Observer2: Listens for element insertions */
var observer2 = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes) {
[].slice.call(mutation.addedNodes).forEach(function(node) {
if (node.nodeType === Node.ELEMENT_NODE) {
modifyElems(node);
}
});
}
});
});
/* Start observing 'body' for 'div#search' */
observer1.observe(document.body, config);
Your doFilter() function only runs once when the page initially loads, which means that if Google loads in any results using AJAX (as it often does), your code will not affect them.
How about having your extension add a <style> element to the page head with the styles you want?
<style>
.rc { background-color: green; text-align: center; }
</style>
This has the added benefit of not blasting away any style attributes that the target elements might already have.
$(window).load(function() {
doFilter(document.body);;
});
Instead of:
window.addEventListener("DOMContentLoaded", function() {
doFilter(document.body);
});
solved my problem.
I also had to download jquery.js library file and refer it within the manifest here:
"js": ["jquery.js", "content_scripts.js"],
There is a lot of confusion dealing with jQuery functions. As far as I could read, $(window).load() is the one which is executed once the page is fully created.
Reference: jQuery - What are differences between $(document).ready and $(window).load?
Thank you #JLRishe for the response.
I'm writing a Chrome Extension and am having some difficulty getting more than 5 tabs open. here is the source code.
manifset.json
{
"manifest_version": 2,
"name": "Test",
"description": "testing this",
"version": "1.0",
"browser_action": {
"default_icon": "icon.png",
"default_popup": "popup.html"
},
"permissions": [
"bookmarks"
]
}
popup.html
<!doctype html>
<html>
<head>
<title>Testing</title>
<style>
body {
min-width: 357px;
overflow-x: hidden;
}
</style>
<script src="popup.js"></script>
</head>
<body>
</body>
</html>
popup.js
document.addEventListener('DOMContentLoaded', function () {
chrome.bookmarks.getTree(function (stuff){
traverseBookmarks(stuff[0].children[0].children);
});
});
function traverseBookmarks(bookmarkTreeNodes) {
for(var i=0;i<bookmarkTreeNodes.length;i++) {
var bookmark = document.createElement('a');
if(bookmarkTreeNodes[i].url){
bookmark.href = bookmarkTreeNodes[i].url;
bookmark.target = "_blank";
}
else{
(function(num) {
bookmark.addEventListener("click", function() {
addChildren(bookmarkTreeNodes[num].children, false );
})})(i);
document.body.appendChild(document.createElement("br"));
}
bookmark.innerHTML = bookmarkTreeNodes[i].title;
document.body.appendChild(bookmark);
document.body.appendChild(document.createElement("br"));
if(bookmarkTreeNodes[i].children) {
traverseBookmarks(bookmarkTreeNodes[i].children);
}
}
}
function addChildren(children) {
for(var i = 0; i < children.length; i++){
// will open each link in the current window
chrome.tabs.create({
url: children[i].url
});
}
}
The goal is to be able to click the folder(now just a link symbolizing the folder) and open all links inside that folder. Currently what happens in I click on the link to the folder and it opens the first 5. For the sake of getting some form of logging I added:
var bookmark = document.createElement('a');
bookmark.innerHTML = children[i].title;
document.body.appendChild(bookmark);
document.body.appendChild(document.createElement("br"));
to the addChildren() function. It prints out every child. The issue I'm running into is that when I click on the folder it only opens up the first 5 tabs then I'm guessing the focus leaves the popup so it doesn't finish. I can't really find anything else online to help
Any help is appreciated. Let me know if I need to clarify anything.
I suspect the problem is just as you said, when you create a new tab it is active by default so the popup loses focus and unloads. You could fix this in one of two ways. You can send the list of url's to a background page and have that open them all. This will work because the background page stays loaded when opening tabs.
Another, probably better, way to fix it would be to create the tabs with the active property set to false and then maybe switch to the first tab in the group after they are all open. Code:
function addChildren(children) {
for(var i = 0; i < children.length; i++){
chrome.tabs.create({
url: children[i].url,
active: false
});
}
// switch focus to first opened tab
chrome.tabs.query({url:children[0].url},function(tabs){
chrome.tabs.update(tabs[tabs.length-1].id,{active:true});
});
}
i looked everywhere trying to find an answer to this question.
i want my extension to either disable all javascript on the page BUT to allow the insertion of a cotent script that will work. (so chrome.contentSettings.javascript is not a valid option for now)
Alternatively i want a way to remove all script tags before any of them fire (which is kinda the same thing)
i tried inserting content scripts to runat:document_start but the dom is not fully there at the time. itried adding a conte t s ript on tabs.onUpdate when state is loading but that is too late and as well as content scripts at document_end (all of which who try to remove script tags) but it is still too late.
in an act of desperation i tried altering the behavior of the getters and setters of element.innerHTML to. remove the tags but that did not work as well
i am trying to avoid sending an xhr request to location.href and parse and re_set the content as that is too intensive.
any ideas?
After seeing your comments I think this might suit your needs. It works by getting the page's source, render it to a DOM, disable all the JS and then put it back into the page. Not exactly what you wanted but should suit your case well...
mainfest.json
{
"name": "Reload and Kill JS - Using a content script",
"version": "1.0",
"permissions": [
"tabs", "<all_urls>" , "storage"
],
"background": {
"scripts": ["background.js"]
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["injectedCode.js"],
"run_at" : "document_start"
}
],
"minimum_chrome_version" : "20",
"manifest_version" : 2
}
background.js
chrome.storage.local.set({"blockhttp://paez.kodingen.com/":true});
injectedCode.js
reloadAndKillJS = function() {
document.documentElement.innerHTML = 'Reloading Page...';
var xhr = new XMLHttpRequest();
xhr.open('GET', window.location.href, true);
xhr.onerror = function() {
document.documentElement.innerHTML = 'Error getting Page';
}
xhr.onload = function() {
var page = document.implementation.createHTMLDocument("");
page.documentElement.innerHTML = this.responseText;
var newPage = document.importNode(page.documentElement,true);
var nodeList = newPage.querySelectorAll('script');
for (var i = 0; i < nodeList.length; ++i) {
var node = nodeList[i];
if (node.src) {
node.setAttribute('original-src', node.src);
node.removeAttribute('src');
}
node.innerText = '';
}
document.replaceChild(newPage, document.documentElement);
delete page;
// Do your thing here
}
xhr.send();
}
chrome.storage.local.get("block"+window.location.href,function(items)
{
if (items["block"+window.location.href]){
window.stop();
reloadAndKillJS();
}
});
Well, the only way to truly prevent scripts is with contentSettings. So you need to put your code somewhere else, in another domain, since contentSettings rules can be applied for specific URL's.
Put you content script to run at document start.
contentScript.js:
window.stop();
document.all[0].innerHTML = "\
<html>\
<body>\
<iframe src=\"chrome-extension://ID/inject.html?url="+encodeURIComponent(location.href)+"\"></iframe>\
</body>\
</html>";
inject.html:
<html>
<head>
<script>
var frame = document.querySelector('iframe');
frame.src = location.search.replace('?url=', '');
frame.onload = function() {
//Your stuff here
}
</script>
</head>
<body>
<iframe></iframe>
</body>
<html>
Now your code is in a parent frame and in another domain, but it may cause some CORS issues which you can try found some workarounds later.
Give a try, then tell me if there's something to fix.
For instance, if I input:
http://www.google.com/
It would return:
http://www.google.com/images/logos/ps_logo2.png
Using javascript/jquery. These sites would all be external. Thank you!
Since that is a Google Chrome extension, you are not bound to same origin policy.
Basically, you would need content scripts to fetch all the images within a page, and check each image's size within the DOM to know if its larger the last fetched image.
You can use Message Passing, to communicate from the Content Script to the popup/background page.
For example, I will show you how to get the largest image from a page and show it within the popup. We use all the techniques shown above and you will see the largest image within the popup if you activate it. (should show I believe :))
manifest.json (snippet)
...
"permissions": [
"tabs",
"http://*/*"
],
"content_scripts": [
{
"matches": ["http://*/*"],
"js": ["images.js"],
"run_at": "document_start",
"all_frames": true
}
]
...
popup.html
<!DOCTYPE html>
<html>
<head>
<script>
function getImage() {
chrome.tabs.getSelected(null, function(tab) {
chrome.tabs.sendRequest(tab.id, {method: "getImage"}, function (response) {
var text = document.getElementById('image');
var image = response.data;
text.src = image ? response.data : 'no_image.gif';
});
});
}
</script>
</head>
<body>
<button onclick="getImage(); ">Get Largest Image</button>
<img src="no_image.gif" id="image"/>
</body>
</html>
images.js (content script)
function getMaxImage() {
var maxDimension = 0;
var maxImage = null;
// Iterate through all the images.
var imgElements = document.getElementsByTagName('img');
for (var index in imgElements) {
var img = imgElements[index];
var currDimension = img.width * img.height;
if (currDimension > maxDimension){
maxDimension = currDimension
maxImage = img;
}
}
// Check if an image has been found.
if (maxImage)
return maxImage.src;
else
return null;
}
// Listen for extension requests.
chrome.extension.onRequest.addListener(function(request, sender, sendResponse) {
if (request.method == "getImage") {
sendResponse({data: getMaxImage()});
} else {
sendResponse({}); // snub them.
}
});
This is more web-scraping than JavaScript/jQuery.
However, given an assumption that you've received the HTML, and that it is available somehow in a JavaScript string, then something like the following might suffice for finding the maximum dimension image:
var sHTML = getHTMLSomehow(sURL);
var nMaxDim = 0;
var $pageDOM = $(sHTML);
var $objMaxDimImage;
$pageDOM.("img").each(function(){
var $this = $(this);
var nDim = parseFloat($this.width()) * parseFloat($(this).height());
if (nDim > nMaxDim){
$objMaxDimImage = $this;
nMaxDim = nDim
}
});
alert("Max dim is:" nMaxDim);
alert("Image Source:" $objMaxDimImage.attr("src"));
due to the same origin policy you cant access an external site with javascript. maybe you can write a server-side script that downloads the page (for example using wget), search for img-tags in the html code and load all found images to check the size.