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.
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 am trying to make a chrome extension that changes the css of a website. The css is loaded from pastebin with Ajax. The problem is that the old website is shown before the new css is shown- how can I make it so that only the website with changed css is shown?
Content.js:
var s = document.createElement('style');
s.setAttribute("type", "text/css");
var r;
$('head').ready(function() {
$.get("https://pastebin.com/raw/css-file", function(r) {
s.innerHTML = r;
document.head.appendChild(s);
});
});
manifest.json (relevant):
{
"permissions": [
"https://scratch.mit.edu/*",
"https://pastebin.com/raw/*"
],
"content_scripts": [
{
"matches": [
"https://scratch.mit.edu/*",
"http://scratch.mit.edu/*"
],
"js": ["jquery-2.2.2.min.js", "content.js"]
}
]
}
One option is to prevent the body from being visible until the new style is loaded, like this:
$('body').hide();
var s = document.createElement('style');
s.setAttribute("type", "text/css");
var r;
$('head').ready(function() {
$.get("https://pastebin.com/raw/css-file", function(r) {
s.innerHTML = r;
document.head.appendChild(s);
$('body').show();
});
});
But this has a side problem in that you don't know for sure how long the $.get() call will take, and if it takes a long time to return, the page will just be blank.
So instead of hiding the body, a better solution might be to temporarily replace the body html, something like this...
// Save the original body, so we can put it back later
var originalBody = $('body').html();
// Replace the body with a please wait message (use whatever you want)
var pleaseWaitBody = '<div style="text-align: center; margin-top: 100px;">Loading, please wait...</div>';
$('body').html(pleaseWaitBody);
var s = document.createElement('style');
s.setAttribute("type", "text/css");
var r;
$('head').ready(function() {
$.get("https://pastebin.com/raw/css-file", function(r) {
s.innerHTML = r;
document.head.appendChild(s);
// Now that the new style is applied, put the original body back
$('body').html(originalBody);
});
});
EDIT:
The code above won't work for Chrome extensions. However, there is a nother StackOverflow answer that can help. Please see How to hide everything before page load with Chrome Extension
I have made a little Chrome extension that injects some code in the current page.
This extension has a weird behaviour though, whenever the code is injected, none of the page's Javascript triggers seem to work anymore.
Would one of you have any idea what that happens? On top of fixing the code I'd really like to know why this happens.
Example : on this page : http://www.acti.fr/success-story/ghd/ if the extension injects the picture, I cannot click on either the menu or "continuer la lecture" at the bottom.
Here are the manifest and the actual code :
manifest.json
{
"manifest_version": 2,
"name": "wpi",
"description": "just an other extension",
"version": "1.0",
"content_scripts": [{
"matches": ["http://*/*", "https://*/*"],
"js": ["my-style.js"]
}]
}
my-script.js :
function wpkm_check_content(wpkm_text) {
var wpkm_word = wpkm_text.split(" ");
var wpkm_c = wpkm_word[0].localeCompare("Wordpress");
if (wpkm_c == 1)
return (1);
return (0);
}
var wpkm_html = '<div id="wpkm-bloc" style="position:absolute;right:10px;top:10px;z-index:99999">';
wpkm_html += '<img id="wpkm-img" src="https://nathanarnold.files.wordpress.com/2009/02/ssim51.gif">';
wpkm_html += '</div>';
var wpkm_sdomain = document.domain;
var wpkm_request = new XMLHttpRequest();
wpkm_request.open('GET', '/license.txt', true);
wpkm_request.onreadystatechange = function(){
if (wpkm_request.readyState === 4){
if (wpkm_request.status === 200
&& wpkm_check_content(wpkm_request.responseText) == 1) {
document.body.innerHTML += wpkm_html;
}
else {
console.log("Oh no, it does not exist!");
}
}
};
wpkm_request.send();
Any hints will be appreciated :D
You're effectively reassigning the entire innerHTML of the document body by using += append operator which causes reevaluation and recreation of the entire page and of course all previously attached event handlers aren't reattached automatically.
Use insertAdjacentHTML instead:
document.body.insertAdjacentHTML("beforeend", wpkm_html);
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.
This problem seems to have been sort of resolved, as long as the URL of the page you're injecting your javascript into starts with www. What do you do if it doesn't? Here's the relevant part of my manifest:
"content_scripts": [
{
"run_at": "document_start",
"matches": ["https://groups.google.com/forum/?fromgroups=#!newtopic/opencomments-site-discussions"],
"js": ["postMsg.js"]
}
],
The problem, according to another stackoverflow post, is because the URL of the page doesn't begin with 'www'. Does that mean that you can't inject javascript into secure pages whose URL doesn't begin with 'www', or is there another way? This had never been a problem in the past, because my extension had run with Version 1 manifests.
Forgot to add the content script:
var subject = document.getElementById("p-s-0");
subject.setAttribute("value", "foo");
The element with ID "p-s-0" is the Subject field in the Google Groups Post page, so the field should display "foo".
A few issues:
That is a not valid match pattern because they only specify up to the URL path (the part before the ?).
Change the matches to:
"matches": ["https://groups.google.com/forum/*"],
The overall URL (https://groups.google.com/forum/?fromgroups=#!newtopic/opencomments-site-discussions) is not practical because Google changes the URL parameters willy nilly. For example, fromgroups is not often present, and may not have the = if it is. Additional parameters, like hl=en come and go. (This is the reason why my earlier answer worked for me, but not for you.)
So, using include_globs in the manifest would be a messy, error-prone exercise.
The solution is to checklocation.hash within the content script.
The script is set to "run_at": "document_start", so the content script is running before there is any node with id p-s-0.
Change the manifest to "run_at": "document_end".
The new Google groups is heavily AJAX driven. So, The "New Topic" page is usually "loaded" without actually loading a whole new page. This means the content script will not rerun. It needs to monitor for "new" AJAX-loaded pages.
Check for "new" pages by monitoring the hashchange event.
Additionally, the p-s-0 element is added by AJAX, and is not immediately available on a "new" page. Check for this element within a setInterval.
Putting it all together,
The manifest.json becomes:
{
"manifest_version": 2,
"content_scripts": [ {
"run_at": "document_end",
"js": [ "postMsg.js" ],
"matches": [ "https://groups.google.com/forum/*" ]
} ],
"description": "Fills in subject when posting a new topic in select google groups",
"name": "Google groups, Topic-subject filler",
"version": "1"
}
The content script(postMsg.js) becomes:
fireOnNewTopic (); // Initial run on cold start or full reload.
window.addEventListener ("hashchange", fireOnNewTopic, false);
function fireOnNewTopic () {
/*-- For the pages we want, location.hash will contain values
like: "#!newtopic/{group title}"
*/
if (location.hash) {
var locHashParts = location.hash.split ('/');
if (locHashParts.length > 1 && locHashParts[0] == '#!newtopic') {
var subjectStr = '';
switch (locHashParts[1]) {
case 'opencomments-site-discussions':
subjectStr = 'Site discussion truth';
break;
case 'greasemonkey-users':
subjectStr = 'GM wisdom';
break;
default:
break;
}
if (subjectStr) {
runPayloadCode (subjectStr);
}
}
}
}
function runPayloadCode (subjectStr) {
var targetID = 'p-s-0'
var failsafeCount = 0;
var subjectInpTimer = setInterval ( function() {
var subject = document.getElementById (targetID);
if (subject) {
clearInterval (subjectInpTimer);
subject.setAttribute ("value", subjectStr);
}
else {
failsafeCount++;
//console.log ("failsafeCount: ", failsafeCount);
if (failsafeCount > 300) {
clearInterval (subjectInpTimer);
alert ('Node id ' + targetID + ' not found!');
}
}
},
200
);
}