Capture (screenshot) of inactive tab - javascript

Would like to capture image of possibly inactive tab.
Problem is that when using the approach displayed here the tab often does not get time to load before capture is done, resulting in failure.
The chrome.tabs.update() call-back is executed before the tab can be captured.
I have also tried to add listeners to events like tabs.onActivated and tabs.onHighlighted and doing the capture when those are fired, but the result is the same. And, as a given by that, I have also tried highlighted instead of active on chrome.tabs.update() – and combination of both; with listeners and call-backs.
The only way to make it work partially better is by using setTimeout() , but that is very hackish, not reliable and ugly. The fact one have to activate a tab before capture is somewhat noisy – but if one have to add delays the issue becomes somewhat worse.
This is more like a convenience feature for my extension, but would be nice to make it work.
/* Not the real code, but the same logic. */
var request_tab = 25,
request_win = 123
old_tab;
/* Check which tab is active in requested window. */
chrome.tabs.query({
active : true,
windowId : request_win
}, function (re) {
old_tab = re[0].id;
if (old_tab !== request_tab) {
/* Requested tab is inactive. */
/* Activate requested tab. */
chrome.tabs.update(request_tab, {
active: true
}, function () {
/* Request capture */ /* CB TOO SOON! */
chrome.tabs.captureVisibleTab(request_window, {
format : 'png'
}, function (data) {
/* ... data ... */
/* Put back old tab */
chrome.tabs.update(old_tab, {
active: true
});
})
});
} else {
/* Requested tab is active. */
/* Request capture. */
chrome.tabs.captureVisibleTab(request_window, {
format : 'png'
}, function (data) {
/* ... data ... */
})
}
});

Since that you are updating the tab using the chrome.tabs.update() method, the callback will be called as soon as the tab properties are changed, but, obviously, before the page is loaded. To work around this you should remember that the tab isn't yet ready and, using the chrome.tabs.onUpdated event, check when it's ready and you can use chrome.tabs.captureVisibleTab().
Here is the solution:
var request_tab = 25,
request_win = 123,
waiting = false,
// ^^^ Variable used to check if tab has loaded
old_tab;
// Check which tab is active in requested window.
chrome.tabs.query({
active : true,
windowId : request_win
}, function (re) {
old_tab = re[0].id;
if (old_tab !== request_tab) {
// Requested tab is inactive
// Activate requested tab
chrome.tabs.update(request_tab, { active: true });
// Tab isn't ready, you can't capture yet
// Set waiting = true and wait...
waiting = true;
} else {
// Requested tab is active
// Request capture
chrome.tabs.captureVisibleTab(request_window, {
format : 'png'
}, function (data) {
// Elaborate data...
})
}
});
chrome.tabs.onUpdated.addListener(function(tabID, info, tab) {
// If the tab wasn't ready (waiting is true)
// Then check if it's now ready and, if so, capture
if (waiting && tab.status == "complete" && tab.id == request_tab) {
// Now you can capture the tab
chrome.tabs.captureVisibleTab(request_window, {
format : 'png'
}, function (data) {
// Elaborate data...
// Put back old tab
// And set waiting back to false
chrome.tabs.update(old_tab, { active: true });
waiting = false;
});
}
});

Related

chrome.tabs.update stops working when called from extension

I tried the following code. It basically takes a screenshot from all tabs open in the current window:
function captureWindowTabs(windowId, callbackWithDataUrlArray) {
var dataUrlArray = [];
// get all tabs in the window
chrome.windows.get(windowId, { populate: true }, function(windowObj) {
var tabArray = windowObj.tabs;
// find the tab selected at first
for(var i = 0; i < tabArray.length; ++i) {
if(tabArray[i].active) {
var currentTab = tabArray[i];
break;
}
}
// recursive function that captures the tab and switches to the next
var photoTab = function(i) {
chrome.tabs.update(tabArray[i].id, { active: true }, function() {
chrome.tabs.captureVisibleTab(windowId, { format: "png" }, function(dataUrl) {
// add data URL to array
dataUrlArray.push({ tabId:tabArray[i].id, dataUrl: dataUrl });
// switch to the next tab if there is one
if(tabArray[i+1]) {
photoTab(i+1);
}
else {
// if no more tabs, return to the original tab and fire callback
chrome.tabs.update(currentTab.id, { active: true }, function() {
callbackWithDataUrlArray(dataUrlArray);
});
}
});
});
};
photoTab(0);
});
}
When I call this code from popup.html opened as a webpage, it works as expected (I trigger this from a button click in the popup.html). When I call it from the browser extension, it just gets interrupted from the first tab it selects. Any idea why that is? I can't share errors, since the debugger gets closed when called from the extension.
Supplementary, is there a way to achieve desired result without needing the visual tab switching?
While updating the next tab as active tab. make sure current tab is no more active tab by doing
chrome.tabs.update(tabArray[i-1].id, { active: false }, ()=>{});
Moving the extension to a background script fixed the problem.
Reasoning is that the popup will close once the tab switches. Hence it is required to run in the background where it is not interrupted when the popup closes.

detect when challenge window is closed for Google recaptcha

I am using Google invisible recaptcha. Is there a way to detect when the challenge window is closed? By challenge window I mean window where you have to pick some images for verification.
I currently put a spinner on the button that rendered the recaptcha challenge, once the button is clicked. There is no way for the user to be prompted with another challenge window.
I am calling render function programmatically:
grecaptcha.render(htmlElement, { callback: this.verified, expiredCallback: this.resetRecaptcha, sitekey: this.siteKey, theme: "light", size: "invisible" });
I have 2 callback functions wired up the verified and the resetRecaptcha functions which look like:
function resetRecaptcha() {
grecaptcha.reset();
}
function verified(recaptchaResponse)
{
/*
which calls the server to validate
*/
}
I would have expected that grecaptcha.render has another callback that is called when the challenge screen is closed without the user verifying himself by selecting the images.
As you mention, the API doesn't support this feature.
However, you can add this feature yourself. You may use the following code with caution, Google might change its reCaptcha and by doing so break this custom code. The solution relies on two characteristics of reCaptcha, so if the code doesn't work, look there first:
the window iframe src: contains "google.com/recaptcha/api2/bframe"
the CSS opacity property: changed to 0 when the window is closed
// to begin: we listen to the click on our submit button
// where the invisible reCaptcha has been attachtted to
// when clicked the first time, we setup the close listener
recaptchaButton.addEventListener('click', function(){
if(!window.recaptchaCloseListener) initListener()
})
function initListener() {
// set a global to tell that we are listening
window.recaptchaCloseListener = true
// find the open reCaptcha window
HTMLCollection.prototype.find = Array.prototype.find
var recaptchaWindow = document
.getElementsByTagName('iframe')
.find(x=>x.src.includes('google.com/recaptcha/api2/bframe'))
.parentNode.parentNode
// and now we are listening on CSS changes on it
// when the opacity has been changed to 0 we know that
// the window has been closed
new MutationObserver(x => recaptchaWindow.style.opacity == 0 && onClose())
.observe(recaptchaWindow, { attributes: true, attributeFilter: ['style'] })
}
// now do something with this information
function onClose() {
console.log('recaptcha window has been closed')
}
As I mentioned in the comments of the answer submitted by #arcs, it is a good solution which works but it also fires onClose() when the user successfully completes the challenge. My solution is to change the onClose() function like so:
// now do something with this information
function onClose() {
if(!grecaptcha.getResponse()) {
console.log('recaptcha window has been closed')
}
}
This way, it only executes the desired code if the challenge has been closed and it has not been completed by the user, thus the response cannot be returned with grecaptcha.getResponse()
The drawback of detecting when iframe was hidden is that it fires not only when user closes captcha by clicking in background, but also when he submits the answer.
What I needed is detect only the first situation (cancel captcha).
I created a dom observer to detect when captcha is attached to DOM, then I disconnect it (because it is no longer needed) and add click handler to its background element.
Keep in mind that this solution is sensitive to any changes in DOM structure, so if google decides to change it for whatever reason, it may break.
Also remember to cleanup the observers/listeners, in my case (react) I do it in cleanup function of useEffect.
const captchaBackgroundClickHandler = () => {
...do whatever you need on captcha cancel
};
const domObserver = new MutationObserver(() => {
const iframe = document.querySelector("iframe[src^=\"https://www.google.com/recaptcha\"][src*=\"bframe\"]");
if (iframe) {
domObserver.disconnect();
captchaBackground = iframe.parentNode?.parentNode?.firstChild;
captchaBackground?.addEventListener("click", captchaBackgroundClickHandler);
}
});
domObserver.observe(document.documentElement || document.body, { childList: true, subtree: true });
To work in IE this solution needs polyfills for .include() and Array.from(), found below:
Array.from on the Internet Explorer
ie does not support 'includes' method
And the updated code:
function initListener() {
// set a global to tell that we are listening
window.recaptchaCloseListener = true
// find the open reCaptcha window
var frames = Array.from(document.getElementsByTagName('iframe'));
var recaptchaWindow;
frames.forEach(function(x){
if (x.src.includes('google.com/recaptcha/api2/bframe') ){
recaptchaWindow = x.parentNode.parentNode;
};
});
// and now we are listening on CSS changes on it
// when the opacity has been changed to 0 we know that
// the window has been closed
new MutationObserver(function(){
recaptchaWindow.style.opacity == 0 && onClose();
})
.observe(recaptchaWindow, { attributes: true, attributeFilter: ['style'] })
}
My solution:
let removeRecaptchaOverlayEventListener = null
const reassignGRecatchaExecute = () => {
if (!window.grecaptcha || !window.grecaptcha.execute) {
return
}
/* save original grecaptcha.execute */
const originalExecute = window.grecaptcha.execute
window.grecaptcha.execute = (...params) => {
try {
/* find challenge iframe */
const recaptchaIframe = [...document.body.getElementsByTagName('iframe')].find(el => el.src.match('https://www.google.com/recaptcha/api2/bframe'))
const recaptchaOverlay = recaptchaIframe.parentElement.parentElement.firstElementChild
/* detect when the recaptcha challenge window is closed and reset captcha */
!removeRecaptchaOverlayEventListener && recaptchaOverlay.addEventListener('click', window.grecaptcha.reset)
/* save remove event listener for click event */
removeRecaptchaOverlayEventListener = () => recaptchaOverlay.removeEventListener('click', window.grecaptcha.reset)
} catch (error) {
console.error(error)
} finally {
originalExecute(...params)
}
}
}
Call this function after you run window.grecaptcha.render() and before window.grecaptcha.execute()
And don't forget to remove event listener: removeRecaptchaOverlayEventListener()
For everybody that didn't quite get how it all works, here is another example with explanations that you might find useful:
So we have 2 challenges here.
1) Detect when the challenge is shown and get the overlay div of the challenge
function detectWhenReCaptchaChallengeIsShown() {
return new Promise(function(resolve) {
const targetElement = document.body;
const observerConfig = {
childList: true,
attributes: false,
attributeOldValue: false,
characterData: false,
characterDataOldValue: false,
subtree: false
};
function DOMChangeCallbackFunction(mutationRecords) {
mutationRecords.forEach((mutationRecord) => {
if (mutationRecord.addedNodes.length) {
var reCaptchaParentContainer = mutationRecord.addedNodes[0];
var reCaptchaIframe = reCaptchaParentContainer.querySelectorAll('iframe[title*="recaptcha"]');
if (reCaptchaIframe.length) {
var reCaptchaChallengeOverlayDiv = reCaptchaParentContainer.firstChild;
if (reCaptchaChallengeOverlayDiv.length) {
reCaptchaObserver.disconnect();
resolve(reCaptchaChallengeOverlayDiv);
}
}
}
});
}
const reCaptchaObserver = new MutationObserver(DOMChangeCallbackFunction);
reCaptchaObserver.observe(targetElement, observerConfig);
});
}
First, we created a target element that we would observe for Google iframe appearance. We targeted document.body as an iframe will be appended to it:
const targetElement = document.body;
Then we created a config object for MutationObserver. Here we might specify what exactly we track in DOM changes. Please note that all values are 'false' by default so we could only leave 'childList' - which means that we would observe only the child node changes for the target element - document.body in our case:
const observerConfig = {
childList: true,
attributes: false,
attributeOldValue: false,
characterData: false,
characterDataOldValue: false,
subtree: false
};
Then we created a function that would be invoked when an observer detects a specific type of DOM change that we specified in config object. The first argument represents an array of Mutation Observer objects. We grabbed the overlay div and returned in with Promise.
function DOMChangeCallbackFunction(mutationRecords) {
mutationRecords.forEach((mutationRecord) => {
if (mutationRecord.addedNodes.length) { //check only when notes were added to DOM
var reCaptchaParentContainer = mutationRecord.addedNodes[0];
var reCaptchaIframe = reCaptchaParentContainer.querySelectorAll('iframe[title*="recaptcha"]');
if (reCaptchaIframe.length) { // Google reCaptcha iframe was loaded
var reCaptchaChallengeOverlayDiv = reCaptchaParentContainer.firstChild;
if (reCaptchaChallengeOverlayDiv.length) {
reCaptchaObserver.disconnect(); // We don't want to observe more DOM changes for better performance
resolve(reCaptchaChallengeOverlayDiv); // Returning the overlay div to detect close events
}
}
}
});
}
Lastly we instantiated an observer itself and started observing DOM changes:
const reCaptchaObserver = new MutationObserver(DOMChangeCallbackFunction);
reCaptchaObserver.observe(targetElement, observerConfig);
2) Second challenge is the main question of that post - how do we detect that the challenge is closed? Well, we need help of MutationObserver again.
detectReCaptchaChallengeAppearance().then(function (reCaptchaChallengeOverlayDiv) {
var reCaptchaChallengeClosureObserver = new MutationObserver(function () {
if ((reCaptchaChallengeOverlayDiv.style.visibility === 'hidden') && !grecaptcha.getResponse()) {
// TADA!! Do something here as the challenge was either closed by hitting outside of an overlay div OR by pressing ESC key
reCaptchaChallengeClosureObserver.disconnect();
}
});
reCaptchaChallengeClosureObserver.observe(reCaptchaChallengeOverlayDiv, {
attributes: true,
attributeFilter: ['style']
});
});
So what we did is we get the Google reCaptcha challenge overlay div with the Promise we created in Step1 and then we subscribed for "style" changes on overlay div. This is because when the challenge is closed - Google fade it out.
It's important to note that the visibility will be also hidden when a person solves the captcha successfully. That is why we added !grecaptcha.getResponse() check. It will return nothing unless the challenge is resolved.
This is pretty much it - I hope that helps :)

How to bookmark and close all tabs with javascript?

I have code so far that will save a bookmark of the current tab then close it when i push my WebExtension button. I want the code to save and then close all of the tabs.
var currentTab;
var currentBookmark;
// gets active tabe
function callOnActiveTab(callback) {
chrome.tabs.query({currentWindow: true}, function(tabs) {
for (var tab of tabs) {
if (tab.active) {
callback(tab, tabs);
}
}
});
}
/*
* Add the bookmark on the current page.
*/
function Bookmark() {
chrome.bookmarks.create({title: currentTab.title, url: currentTab.url}, function(bookmark) {
currentBookmark = bookmark;
});
callOnActiveTab((tab) => {
chrome.tabs.remove(tab.id);
});
}
/*
* Switches currentTab and currentBookmark to reflect the currently active tab
*/
function updateTab() {
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
if (tabs[0]) {
currentTab = tabs[0];
chrome.bookmarks.search({url: currentTab.url}, (bookmarks) => {
currentBookmark = bookmarks[0];
});
}
});
}
function listTabs() {
Bookmark();
}
chrome.browserAction.onClicked.addListener(listTabs);
chrome.tabs.onUpdated.addListener(updateTab);
// listen to tab switching
chrome.tabs.onActivated.addListener(updateTab);
If I add the Bookmark() function to the end of updateTab() function, the button no longer works and when I change tabs it saves that one and exits all tabs.
Quite a bit of your code appears overly complicated for what you appear to be attempting to do. A significant part of your problem with not being able to use the Bookmark function to bookmark and remove multiple tabs is that it is relying on a global variable that is changed by an asynchronous event handler which is tracking the active tab. That function can be re-coded to use an argument that is passed in to the function. In that way it can be generally re-used.
Note: I moved the removal of the tab out of the bookmarkTab function (what is Bookmark in your code). Having it in there, while only calling the function Bookmark, is a bad idea. I added a bookmarkAndRemoveTab() function which is clearly named for both things that it is doing.
Just the sections associated with your browserAction could be:
var currentBookmark;
/* Add a bookmark for a tab
* tabsTab - The tabs.Tab object for the tab containing the page to bookmark
* callback - Called with the tabs.Tab object when the bookmark is complete
*/
function bookmarkTab(tabsTab, callback) {
chrome.bookmarks.create({title: tabsTab.title, url: tabsTab.url}, function(bookmark) {
currentBookmark = bookmark;
if(typeof callback === 'function'){
callback(tabsTab);
}
});
}
/* Remove a Tab
* tabsTab - The tabs.Tab object for the tab to remove
* callback - Called with the, now invalid, tab ID of the removed tab
*/
function removeTab(tabsTab, callback){
let rememberedId = tabsTab.id; //Unknown if object changes upon removal
chrome.tabs.remove(rememberedId,function(){
if(typeof callback === 'function'){
callback(rememberedId);
}
});
}
/* Bookmark and remove a tab once the bookmark has been made
* tabsTab - The tabs.Tab object for the tab to remove
*/
function bookmarkAndRemoveTab(tabsTab) {
//When we get here from the browserAction click, tabsTab is the active tab
// in the window where the button was clicked. But, this function can be used
// anytime you have a tabs.Tab object for the tab you want to bookmark and delete.
bookmarkTab(tabsTab,removeTab);
}
chrome.browserAction.onClicked.addListener(bookmarkAndRemoveTab);
Then you could have a function that did bookmarkAndRemoveTab() on every tab:
/* Bookmark and remove all tabs
*/
function bookmarkAndRemoveAllTabs() {
//Get all tabs in 'normal' windows:
// May want to test. Could want to get all tabs in all windows
// Of windowTypes:["normal","popup","panel","devtools", probably only
// want "normal" and "popup" tabs to be bookmarked and closed.
let queryInfos = [{windowType: 'normal'},{windowType: 'popup'}];
queryInfos.forEach(function(queryInfo){
chrome.tabs.query(queryInfo, function(tabs) {
for (let tab of tabs) {
bookmarkAndRemoveTab(tab);
}
});
});
}

I need to popup different html pages for different url in chrome extension

For example if someone is on google.com I need to popup a different page and if someone is on xyz.com I need to pop a different page. Is that possible?
As suggested by wOxxOm, it may be a better solution to have a single popup page with several sections, and hide/show them as appropriate.
Start with all hidden, and at runtime make a decision:
document.addEventListener("DOMContentLoaded", function() {
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
// Note: this requires "activeTab" permission to access the URL
if(/* condition on tabs[0].url */) {
/* Adapt UI for condition 1 */
} else if (/* ... */) {
/* Adapt UI for condition 2 */
}
});
});
Do note that it's recommended to use Page Actions instead of Browser Actions for things that make sense only on certain pages.
In your background page you can change the page to display in popup.
Use tabs events to get selected tab and current tab url.
Use :
// Update popup url method
var updatePopupURLForSelectedTab = function (selectedTab) {
var popUpURL = DEFAULT_URL_OF_YOUR_HTML_FILE;
var selectedTabURL = selectedTab.url;
if (selectedTabURL.match(/.*\.?google\.com.*/) != null ) {
popUpURL = GOOGLE_URL_OF_YOUR_HTML_FILE;
}
else if (selectedTabURL.match(/.*\.?xyz\.com.*/) != null) {
popUpURL = XYZ_URL_OF_YOUR_HTML_FILE;
}
// Set Popup URL
chrome.browserAction.setPopup({
popup :popUpURL
});
};
// Get current selected Tab
chrome.tabs.getSelected(null, function (tab) {
updatePopupURLForSelectedTab(tab);
});
// Listen for selected tab
chrome.tabs.onActiveChanged.addListener(function(tabId, selectInfo) {
// Get selected tab
chrome.tabs.get(tabId, function (tab) {
updatePopupURLForSelectedTab(tab);
});
});
// Listen navigation update
chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
updatePopupURLForSelectedTab(tab);
});
// Listen for window change
chrome.windows.onFocusChanged.addListener(function (windowId) {
chrome.tabs.getSelected(windowId, function (tab) {
updatePopupURLForSelectedTab(tab);
});
});

Detect if chrome panels are enabled

I would like to detect if panels are enabled in chrome, in javascript.
Currently, you can create a panel with this code:
chrome.windows.create({ url: "[url]", width: 500, height: 516, type: 'panel'});
When panels in chrome are disabled, it opens a popup.
But the problem is that panels are not enabled on every chrome build. but people can enable it by hand on chrome://flags. So when flags are disabled, I want to redirect people to that page so they can enable panels.
You can detect if the opened window is a panel using the alwaysOnTop boolean property in the callback of chrome.windows.create:
chrome.windows.create({
url: '...url...', // ...
type: 'panel'
}, function(windowInfo) {
// if windowInfo.alwaysOnTop is true , then it's a panel.
// Otherwise, it is just a popup
});
If you want to detect whether flags are enabled or not, create the window, read the value, then remove it. Because the creation process is asynchrous, the value retrieval must be implemented using a callback.
var _isPanelEnabled;
var _isPanelEnabledQueue = [];
function getPanelFlagState(callback) {
if (typeof callback != 'function') throw Error('callback function required');
if (typeof _isPanelEnabled == 'boolean') {
callback(_isPanelEnabled); // Use cached result
return;
}
_isPanelEnabledQueue.push(callback);
if (_isPanelEnabled == 'checking')
return;
_isPanelEnabled = 'checking';
chrome.windows.create({
url: 'about:blank',
type: 'panel'
}, function(windowInfo) {
_isPanelEnabled = windowInfo.alwaysOnTop;
chrome.windows.remove(windowInfo.id);
// Handle all queued callbacks
while (callback = _isPanelEnabledQueue.shift()) {
callback(windowInfo.alwaysOnTop);
}
});
}
// Usage:
getPanelFlagState(function(isEnabled) {
alert('Panels are ' + isEnabled);
});
Because the flag can only be toggled by reloading the Chrome browser, it makes sense to cache the value of the flag (as shown in the function). To make sure that the window creation test happens only once, the callbacks are queued.

Categories