Chrome extension and .click() loops using values from localStorage - javascript

I have made a Chrome extension to help using a small search engine in our company's intranet. That search engine is a very old webpage really convoluted, and it doesn't take parameters in the url. No chance that the original authors will assist:
The extension popup offers an input text box to type your query. Your
query is then saved in localStorage
There is a content script inserted in
the intranet page that reads the localStorage key and does a document.getElementById("textbox").value = "your query"; and then does
document.getElementById("textbox").click();
The expected result is that your search is performed. And that's all.
The problem is that the click gets performed unlimited times in an infinite loop, and I cannot see why it's repeating.
I would be grateful if you would be able to assist. This is my first Chrome extension and all what I have been learning about how to make them has been a great experience so far.
This is the relevant code:
The extension popup where you type your query
popup.html
<input type="search" id="cotext"><br>
<input type="button" value="Name Search" id="cobutton">
The attached js of the popup
popup.js
var csearch = document.getElementById("cotext");
var co = document.getElementById("cobutton");
co.addEventListener("click", function() {
localStorage["company"] = csearch.value;
window.open('url of intranet that has content script applied');
});
And now the background file to help with communication between parts:
background.js
chrome.extension.onRequest.addListener(function(request, sender, sendResponse) {
sendResponse({data: localStorage[request.key]});
});
And finally the content script that is configured in the manifest to be injected on the url of that search engine.
incomingsearch.js
chrome.extension.sendRequest(
{method: "getLocalStorage", key: "company"},
function(response) {
var str = response.data;
if (document.getElementById("txtQSearch").value === "") {
document.getElementById("txtQSearch").value = str;
}
document.getElementById("btnQSearch").click();
});
So as I mentioned before, the code works... not just once (as it should) but many many times. Do I really have an infinite loop somewhere? I don't see it... For the moment I have disabled .click() and I have put .focus() instead, but it's a workaround. I would really like to use .click() here.
Thanks in advance!

The loop is probably caused by clicking the button even if it has a value. Try putting it inside your if. That said, you are overcomplicating it.
You can access the extension's data inside content scripts directly by replacing localstorage with the chrome.storage extension api. Add the "storage" (silent) permission to your manifest.json, like this:
"permissions": ["storage"]
Remove the message passing code in background.js. Then replace the popup button listener contents with:
chrome.storage.local.set({ "company": csearch.value }, function() {
chrome.tabs.create({ url: "whatever url" })
})
Replace the content script with:
chrome.storage.local.get("company", function(items) {
if(document.querySelector("#txtQSearch").value == "") {
document.querySelector("#txtQSearch").value = items.company
document.querySelector("#btnQSearch").click()
}
})
document.querySelector() performs the same function here as getElementById, but it is much more robust. It also has less capital letters, which makes it easier to type in my opinion.

Related

Chrome extension: sending data to window created with chrome.windows.create

I'm struggling to find the best way to communicate with my web app, which I'm opening with chrome.windows.create in my extension.
I've got the wiring between content script and background script right. I can right click an element and send it's value to the background script, and the background script creates a window containing my webapp. But from there I can't figure out how to access and use that value in my webapp (it needs to load the value into an editor).
I've tried setting fns and vars on the window and tab objects, but somehow they go missing from the window object once the web app is loaded.
With chrome.tabs.executeScript I can fiddle with the dom, but not set global variables or anything on 'window' either.
If there isn't a better way, I guess I'm forced to add to the DOM and pick that up once my web app is loaded, but it seems messy. I was hoping for a cleaner method, like setting an onLoadFromExtension fn which my web app can execute to get the value it needs.
I found a method that works after much trial and error, though it still seems error prone. And it also depends on the extension ID matching the installed one, so if that can't be hard-coded it'll be another message that needs passing through another channel (after reading up, looks like that can be hard-coded since it's a hash of the public key, so problem solved)... Starting to think manipulating the DOM is less messy...
background.js:
var selectedContent = null;
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
console.info("------------------------------- Got request", request);
if (request.getSelectedContent) {
sendResponse(selectedContent);
}
});
web app:
var extensionId = "naonkagfcedpnnhdhjahadkghagenjnc";
chrome.runtime.sendMessage(extensionId, {getSelectedContent: "true"},
response => {
console.info("----------------- Got response", response);
if(response) {
this.text = response;
}
});
manifest.json:
"externally_connectable": {
"ids": ["naonkagfcedpnnhdhjahadkghagenjnc"],
"matches": ["http://localhost:1338/*"]
},
Within the popup, do the following:
const parentWindow = window.opener
parentWindow.postMessage({ action: 'opened' })
window.onmessage = msg => {
alert(JSON.stringify(msg.data)) // Alerts you with {"your":"data"}
}
Within the script that will call chrome.windows.create, do the following:
window.onmessage = msg => {
if (msg.data.action == 'opened') {
msg.source.postMessage({ your: 'data' })
}
}
Set setSelfAsOpener: true when calling chrome.windows.create
How does this work?
Due to limitations of the Chrome extension windows API, the created window needs to post a message to its creator (aka window.opener) or else the creator won't have access to a WindowProxy (useful for posting messages to the created window).

Executing code at page-level from Background.js and returning the value

I've got a web page with its own scripts and variables that I need to execute and retrieve return values from my extension's Background.js.
I understand (I think!) that in order to interact with the web page, it must be done via chrome.tabs.executeScript or a ContentScript, but because the code must execute in the context of the original page (in order to have scope to the scripts and variables), it needs to be injected into the page first.
Following this great post by Rob W, I'm able to invoke the page-level script/variables, but I'm struggling to understand how to return values in this way.
Here's what I've got so far...
Web page code (that I want to interact with):
<html>
<head>
<script>
var favColor = "Blue";
function getURL() {
return window.location.href;
}
</script>
</head>
<body>
<p>Example web page with script content I want interact with...</p>
</body>
</html>
manifest.json:
{
// Extension ID: behakphdmjpjhhbilolgcfgpnpcoamaa
"name": "MyExtension",
"version": "1.0",
"manifest_version": 2,
"description": "My Desc Here",
"background": {
"scripts": ["background.js"]
},
"icons": {
"128": "icon-128px.png"
},
"permissions": [
"background",
"tabs",
"http://*/",
"https://*/",
"file://*/", //### (DEBUG ONLY)
"nativeMessaging"
]
}
background.js
codeToExec = ['var actualCode = "alert(favColor)";',
'var script = document.createElement("script");',
' script.textContent = actualCode;',
'(document.head||document.documentElement).appendChild(script);',
'script.parentNode.removeChild(script);'].join('\n');
chrome.tabs.executeScript( tab.id, {code:codeToExec}, function(result) {
console.log('Result = ' + result);
} );
I realise the code is currently just "alerting" the favColor variable (this was just a test to make sure I could see it working). However, if I ever try returning that variable (either by leaving it as the last statement or by saying "return favColor"), the executeScript callback never has the value.
So, there appear to be (at least) three levels here:
background.js
content scripts
actual web page (containing scripts/variables)
...and I would like to know what is the recommended way to talk from level 1 to level 3 (above) and return values?
Thanks in advance :o)
You are quite right in understanding the 3-layer context separation.
A background page is a separate page and therefore doesn't share JS or DOM with visible pages.
Content scripts are isolated from the webpage's JS context, but share DOM.
You can inject code into the page's context using the shared DOM. It has access to the JS context, but not to Chrome APIs.
To communicate, those layers use different methods:
Background <-> Content talk through Chrome API.
The most primitive is the callback of executeScript, but it's impractical for anything but one-liners.
The common way is to use Messaging.
Uncommon, but it's possible to communicate using chrome.storage and its onChanged event.
Page <-> Extension cannot use the same techniques.
Since injected page-context scripts do not technically differ from page's own scripts, you're looking for methods for a webpage to talk to an extension. There are 2 methods available:
While pages have very, very limited access to chrome.* APIs, they can nevertheless use Messaging to contact the extension. This is achieved through "externally_connectable" method.
I have recently described it in detail this answer. In short, if your extension declared that a domain is allowed to communicate with it, and the domain knows the extension's ID, it can send an external message to the extension.
The upside is directly talking to the extension, but the downside is the requirement to specifically whitelist domains you're using this from, and you need to keep track of your extension ID (but since you're injecting the code, you can supply the code with the ID). If you need to use it on any domain, this is unsuitable.
Another solution is to use DOM Events. Since the DOM is shared between the content script and the page script, an event generated by one will be visible to another.
The documentation demonstrates how to use window.postMessage for this effect; using Custom Events is conceptually more clear.
Again, I answered about this before.
The downside of this method is the requirement for a content script to act as a proxy. Something along these lines must be present in the content script:
window.addEventListener("PassToBackground", function(evt) {
chrome.runtime.sendMessage(evt.detail);
}, false);
while the background script processes this with a chrome.runtime.onMessage listener.
I encourage you to write a separate content script and invoke executeScript with a file attribute instead of code, and not rely on its callback. Messaging is cleaner and allows to return data to background script more than once.
The approach in Xan's answer (using events for communication) is the recommended approach. Implementing the concept (and in a secure way!) is however more difficult.
So I'll point out that it is possible to synchronously return a value from the page to the content script. When a <script> tag with an inline script is inserted in the page, the script is immediately and synchronously executed (before the .appendChild(script) method returns).
You can take advantage of this behavior by using the injected script to assign the result to a DOM object which can be accessed by the content script. For example, by overwriting the text content of the currently active <script> tag. The code in a <script> tag is executed only once, so you can assign any rubbish to the content of the <script> tag, because it won't be parsed as code any more. For example:
// background script
// The next code will run as a content script (via chrome.tabs.executeScript)
var codeToExec = [
// actualCode will run in the page's context
'var actualCode = "document.currentScript.textContent = favColor;";',
'var script = document.createElement("script");',
'script.textContent = actualCode;',
'(document.head||document.documentElement).appendChild(script);',
'script.remove();',
'script.textContent;'
].join('\n');
chrome.tabs.executeScript(tab.id, {
code: codeToExec
}, function(result) {
// NOTE: result is an array of results. It is usually an array with size 1,
// unless an error occurs (e.g. no permission to access page), or
// when you're executing in multiple frames (via allFrames:true).
console.log('Result = ' + result[0]);
});
This example is usable, but not perfect. Before you use this in your code, make sure that you implement proper error handling. Currently, when favColor is not defined, the script throws an error. Consequently the script text is not updated and the returned value is incorrect. After implementing proper error handling, this example will be quite solid.
And the example is barely readable because the script is constructed from a string. If the logic is quite big, but the content script in a separate file and use chrome.tabs.executeScript(tab.id, {file: ...}, ...);.
When actualCode becomes longer than a few lines, I suggest to wrap the code in a function literal and concatenate it with '(' and ')(); to allow you to more easily write code without having to add quotes and backslashes in actualCode (basically "Method 2b" of the answer that you've cited in the question).
chrome.browserAction.onClicked.addListener(function(tab) {
// No tabs or host permissions needed!
console.log('Turning ' + tab.url + ' red!');
chrome.tabs.executeScript({
file: 'index.js'
});
});
Here index.js is normal js file to inject in browser
#index.js
alert("Hello from api");

Prompt on URL visit

I'm new to Javascript and Chrome extensions, and I'm working on one that triggers a prompt when the user visits a certain webpage, but It's not prompting when the user visits the webpage.
Here's a simplified version that reproduces the problem:
chrome.tabs.query({'active': true, 'lastFocusedWindow': true}, function (tabs) {
var url = tabs[0].url;
if (url === "example.com") {
confirm("You are visiting " + URL);
}
});
What I was hoping this would do is trigger the prompt when the user visits 'Example.com', but I think that I'm misunderstanding how Chrome pulls the URL from the active tab, and maybe how the event is triggered...
Any tips are appreciated.
In your answer, you quoted this question, and yet you picked a weird answer out of the bunch of answers there.
Instead of detecting a page load yourself, it's best to rely on Chrome's content script mechanism, which will ensure that your script is executed in every tab that loads the requested webpage.
If you know the list of the webpages in advance, you can filter by them in the manifest:
"content_scripts" : [{
"matches": ["*://*.example1.com/*", "*://*.example2.com/"],
"js": ["confirm.js"]
}],
And then have a very simple confirm.js:
confirm("You are visiting " + location.href);
This will be very efficient as Chrome will natively filter the requests for you.
If you don't know the list of the webpages in advance, you can use a content script on every page and filter it in your code:
"content_scripts" : [{
"matches": ["*://*/*"],
"js": ["confirm.js"]
}],
And the confirm.js (skeleton):
chrome.storage.local.get("hostFilterData", function(data){
if( checkUrl(location.host, data.hostFilterData) ) {
confirm("You are visiting " + location.href);
}
});
function checkUrl(location.host, filterData){
/* your logic goes here */
}
You might also want to add "run_at" : "document_start" if you want your JS to run at the earliest time possible.
If you really want to keep your logic in the background page, you can listen to various events. chrome.tabs.onUpdated filtered by changeInfo.status == 'complete' should work in most cases, but may not always be a good idea. It will fire very late if the page has a slow-loading resource, and might fire again if the page uses anchor-based navigation (you probably don't want that). Even if you go this route, there's little sense doing a tab query after that, since the event includes the tab ID of the page that fired it.
There's also chrome.webNavigation API providing relevant events.
Try changing
if(url === "example.com")
to
if(url === "example.com" || url === "http://example.com" || url === "https://example.com")

How to detect page title change in Google Chrome from an extension?

I'm creating a Google Chrome extension and I need to detect when a page's title changes. The page's title is changed like in Twitter: (num) Twitter (see the screenshot below) - when a new tweet is posted, the number increments. Example:
I'm trying to detect the title changes of a URL that's loaded in one of my tabs and play a beep sound whenever there's a difference. This check is to be done in a repeated interval and I think that can be accomplished using setTimeOut() function.
I've created a manifest.json as follows:
{
"manifest_version": 2,
"name": "Detect Page Title Changes",
"description": "Blah",
"version": "1.0",
"browser_action": {
"default_icon": "icon.png",
"default_popup": "background.html"
},
"permissions": [
"tabs"
]
}
However, I'm clueless about the rest. I've searched through the docs 1 2 and tried the solutions on similar Stack Overflow threads such as this one I but couldn't find anything that suits my requirements.
Do you have any suggestions? Please include an example, if possible.
Instead of arguing in comments that a certain approach is better, let me be more constructive and add an answer by showing a particular implementation I co-wrote myself, and explain some gotchas you may run into. Code snippets refer to a service different from Twitter, but the goal was the same. In fact, this code's goal is to report the exact number of unread messages, so yours might be simpler.
My approach is based on an answer here on SO, and instead of being polling-driven (check condition at fixed intervals) is event-driven (be notified of potential changes in condition).
Advantages include immediate detection of a change (which would otherwise not be detected until the next poll) and not wasting resources on polls while the condition does not change. Admittedly, the second argument hardly applies here, but the first one still stands.
Architecture at a glance:
Inject a content script into the page in question.
Analyze initial state of the title, report to background page via sendMessage.
Register a handler for a title change event.
Whenever the event fires and the handler is called, analyze the new state of the title, report to background page via sendMessage.
Already step 1 has a gotcha to it. Normal content script injection mechanism, when the content script is defined in the manifest, will inject it in pages upon navigation to a page that matches the URL.
"content_scripts": [
{
"matches": [
"*://theoldreader.com/*"
],
"js": ["observer.js"],
"run_at": "document_idle"
}
]
This works pretty well, until your extension is reloaded. This can happen in development as you're applying changes you've made, or in deployed instances as it is auto-updated. What happens then is that content scripts are not re-injected in existing open pages (until navigation happens, like a reload). Therefore, if you rely on manifest-based injection, you should also consider including programmatic injection into already-open tabs when extension initializes:
function startupInject() {
chrome.tabs.query(
{url: "*://theoldreader.com/*"},
function (tabs) {
for (var i in tabs) {
chrome.tabs.executeScript(tabs[i].id, {file: "observer.js"});
}
}
);
}
On the other end, content script instances that were active at the time of extension reload are not terminated, but are orphaned: any sendMessage or similar request will fail. It is, therefore, recommended to always check for exceptions when trying to communicate with the parent extension, and self-terminate (by removing handlers) if it fails:
try {
chrome.runtime.sendMessage({'count' : count});
} catch(e) { // Happens when parent extension is no longer available or was reloaded
console.warn("Could not communicate with parent extension, deregistering observer");
observer.disconnect();
}
Step 2 also has a gotcha to it, though it depends on the specifics of the service you're watching. Some pages inside the scope of the content script will not show the number of unread items, but it does not mean that there are no new messages.
After observing how the web service works, I concluded that if the title changes to something without navigation, it's safe to assume the new value if correct, but for the initial title "no new items" should be ignored as unreliable.
So, the analysis code accounts for whether it's the initial reading or handling an update:
function notify(title, changed) {
// ...
var match = /^\((\d+)\)/.exec(title);
var match_zero = /^The Old Reader$/.exec(title);
if (match && match[1]) {
count = match[1];
} else if (match_zero && changed) {
count = 0;
}
// else, consider that we don't know the count
//...
}
It is called with the initial title and changed = false in step 2.
Steps 3 & 4 are the main answer to "how to watch for title changes" (in an event-driven way).
var target = document.querySelector('head > title');
var observer = new window.MutationObserver(
function(mutations) {
mutations.forEach(
function(mutation){
notify(mutation.target.textContent, true);
}
);
}
);
observer.observe(target, { subtree: true, characterData: true, childList: true });
For specifics as to why certain options of observer.observe are set, see the original answer.
Note that notify is called with changed = true, so going from "(1) The Old Reader" to "The Old Reader" without navigation is considered to be a "true" change to zero unread messages.
Put chrome.tabs.onUpdated.addListener in your background script:
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
console.log(changeInfo);
});
changeInfo is an object which includes title changes, e.g. here:
Can then filter on the object so that an action only occurs if changeInfo includes a title change. For additional manipulation, e.g. responding to page title changes with page content / actions, you can send a message to content script from inside the listener after whatever conditions are met.
Create an event page.
Create a content script that gets injected into a webpage when a webpage loads.
Within the content script, use setInterval to poll the page to see if window.document.title changes.
If the title has changed, use chrome.runtime.sendMessage to send a message to your event page.
On your event page, listen for messages with chrome.runtime.onMessage and play a sound.
After researching Chrome's tabs API, it doesn't look like anything stands out to help you directly. However, you should be able to attach an event listener to the title node of the tab(s) you're interested in. The DOMSubtreeModified mutation event works in Chrome, and a quick test in a normal html document proves to work for me - should be no different from within an extension.
var title = document.getElementsByTagName('title')[0];
if (title) {
title.addEventListener('DOMSubtreeModified', function (e) {
// title changed
}, false);
}

Google Chrome Extension - Background.html function question

Is there anyway by adding to this javascript I can ingore anything after the .com/ .net/ .org/ etc for tab.url.
So if tab.url = examplesite.com/blabla/blabla.html it will replace tab.url with examplesite.com/ and ignore anything after it.
Here's my background.html script.
<script type="text/javascript">
chrome.browserAction.onClicked.addListener(function(tab) {
chrome.tabs.getSelected(null,function(tab) {
chrome.tabs.create( { url: "http://www.mysite.com/index.php?q=" +tab.url } );
});
});
</script>
Or do I need to program this into mysite to strip the Url? I was wondering if it is possible with Javascript... (not my forte.)
Thank you for any help you may be able to give me.
Unfortunately there is not parseUri function built into javascript but you could build what you're asking for using regular expressions. An example of this can be found here:
http://blog.stevenlevithan.com/archives/parseuri
Also, I've never tried to access it from a Chrome extension, but I suspect you have access to the window.location variable which is an object that contains broken down parts of the current page's url. Trying console.log(window.location) and look at the content of the object.

Categories

Resources