User clicks a button and in click event handler we postMessage to iframe. Iframe handles it in message event handler and calls element.requestFullscreen(). In older browsers it worked (eg. in Chrome 65) but in current (72) it errors with Failed to execute 'requestFullscreen' on 'Element': API can only be initiated by a user gesture..
Is there a way to transfer "gesture initiated" flag in postMessage call?
Note that iframe has allow="fullscreen" attribute.
As always with iframes, it depends on here it is hosted relative to the parent document.
If you are running a same-origin document in that frame, then you can simply call requestFullScreen directly on the element you wish form the main doc:
const frameDoc = frame.contentDocument;
frameDoc.getElementById('el-to-fs').requestFullscreen(); // assuming polyfill
jsfiddle Since StackSnippetsĀ® overly protected iframes won't allow access nor fullscreen.
But if you were, you wouldn't have had this issue in Chrome (you'd still have had in FF).
Because even though it is true that this method requires an user-gesture, postMessage being fast enough, you are able to use it with a same-origin frame in Chrome.
What happens in this browser is that a cross-origin document must have received an user-gesture before being able to call requestFullscreen.
Once the cross-origin frame has been marked as interacted-by-the-user, then you'll be able to call requestFullscreen even from main-doc's events: jsfiddle (still, only in Chrome).
But, for a cross-browser solution, you'd have to
load this content as same-origin (e.g using a proxy)
or to use a small hack where you set the <iframe> in fullscreen mode and let the inner document know you did so, so it can style its document accordingly: jsfiddle
main.js
onclick = e => {
frame.contentWindow.postMessage("you're going fullscreen", '*');
frame.requestFullscreen();
};
frame.js
onmessage = e => {
if(message.data === "you're going fullscreen") {
// trigger our special fullscreen CSS
document.documentElement.classList.add('fullscreen');
// do some DOM cleaning if needed
}
else if(message.data === "you're exiting fullscreen") {
document.documentElement.classList.remove('fullscreen');
}
};
Related
I am trying to mimic what wkwebview does on iOS to an iframe on a webclient.
Essentially what happens is before the iframe is loaded it injects a function the iOS client can read.
on the page loaded within the iframe it calls a function to communicate with the iOS client.
if (window.webkit) {
window.webkit.messageHandlers.interOp.postMessage(chatMsg);
} else if (window.parent) {
window.parent.postMessage(chatMsg, '*');
}
I don't want it to fall into the else window.parent block since postMessage gets quite a bit of traffic from extensions and other applications.
I'd like to add to the Iframe's window object the same webkit.messageHandlers.interOp.postMessage function and listen on that level.
The Iframe domain and the WebClient domain are different.
Another approach would be to only listen to calls to postMessage called by the Iframe. Any ideas are welcome thanks.
If it is a same-domain iframe, then you simply assign a function to the window of the parent, as in:
window.iframeMessageReceiver = msg => alert(`Message received from iframe: ${msg}`)
And you call this function inside of your iframe's if block:
parent.iframeMessageReceiver(chatMsg)
Summary: I need to find a way to accomplish with programmatic injection the same exact behaviour as using content_scripts > matches with "all_frames": true on a manifest. Why? because it is the only way I've found of injecting iframe's content in an extension page without having Cross-Origin errors.
I'm moving to optional_permissions on a Chrome extension and I'm on a dead end.
What I want:
Move this behaviour to optional_permissions in order to be able to add more hosts in the future. With the current code, by adding one new host on content_scripts > matches the extension is disabled by Chrome.
For the move, I removed content_scripts in the manifest and I added "optional_permissions": ["*://*/"],. Then, I successfully implemented a dialog asking new permissions to the user with chrome.permissions.request.
As I said before, the problem is how to inject the iframe's content in an extension page.
What I've tried:
chrome.declarativeContent.RequestContentScript (mentioned here) with allFrames: true. I can only see the script running if I enter the URL directly, nothing happens when that URL is set in an iframe.
chrome.tabs.onUpdated: url is undefined for an extension page. Also, the iframe url is not detected.
Call chrome.tabs.executeScript with allFrames: true as soon as I load the first iframe. By doing this I get an exception Cannot access contents of the page. Extension manifest must request permission to access the respective host. and the "respective host" is chrome-extension://, which is not a valid host if you want to add it to the permissions.
I'm lost. I couldn't find a way to simulate the same behaviour as content_scripts > matches with programmatic injection.
Note: using webNavigation API is not an option since the extension is live and it has thousands of users. Because of this, I can not use the frameId property for executeScript. Thus, my only option with executeScript was to inject all frames but the chrome-extension host issue do not let me continue.
Update: I was able to accomplish what I wanted but only on an HTTP host. I used chrome.tabs.executeScript (option 3).
The question remains on how to make this work on an extension page.
You cannot run content scripts in any extension page, including your own.
If you want to run code in a subframe of your extension page, then you have to use frameId. There are two ways to do this, with and without webNavigation.
I've put all code snippets in this answer together (with some buttons to invoke the individual code snippets) and shared it at https://robwu.nl/s/optional_permissions-script-subframe.zip
To try it out, download and extract the zip file, load the extension at chrome://extensions and click on the extension button to open the test page.
Request optional permissions
Since the goal is to programmatically run scripts with optional permissions, you need to request the permission. My example will use example.com.
If you want to use the webNavigation API too, include its permission in the permission request too.
chrome.permissions.request({
// permissions: ['webNavigation'], // uncomment if you want this.
origins: ['*://*.example.com/*'],
}, function(granted) {
alert('Permission was ' + (granted ? '' : 'not ') + 'granted!');
});
Inject script in subframe
Once you have a tab ID and frameId, injecting scripts in a specific frame is easy. Because of the tabId requirement, this method can only work for frames in tabs, not for frames in your browserAction/pageAction popup or background page!
To demonstrate that code execution succeeds, my examples below will call the next injectInFrame function once the tabId and frameId is known.
function injectInFrame(tabId, frameId) {
chrome.tabs.executeScript(tabId, {
frameId,
code: 'document.body.textContent = "The document content replaced with content at " + new Date().toLocaleString();',
});
}
If you want to run code not just in the specific frame, but all sub frames of that frame, just add allFrames: true to the chrome.tabs.executeScript call.
Option 1: Use webNavigation to find frameId
Use chrome.tabs.getCurrent to find the ID of the tab where the script runs (or chrome.tabs.query with {active:true,currentWindow:true} if you want to know the current tabId from another script (e.g. background script).
After that, use chrome.webNavigation.getAllFrames to query all frames in the tab. The primary way of identifying a frame is by the URL of the page, so you have a problem if the framed page redirects elsewhere, or if there are multiple frames with the same URL. Here is an example:
// Assuming that you already have a frame in your document,
// i.e. <iframe src="https://example.com"></iframe>
chrome.tabs.getCurrent(function(tab) {
chrome.webNavigation.getAllFrames({
tabId: tab.id,
}, function(frames) {
for (var frame of frames) {
if (frame.url === 'https://example.com/') {
injectInFrame(tab.id, frame.frameId);
break;
}
}
});
});
Option 2: Use helper page in the frame to find frameId
The option with webNavigation looks simple but has two main disadvantages:
It requires the webNavigation permission (causing the "Read your browsing history" permission warning)
The identification of the frame can fail if there are multiple frames with the same URL.
An alternative is to first open an extension page that sends an extension message, and find the frameId (and tab ID) in the metadata that is made available in the second parameter of the chrome.runtime.onMessage listener. This code is more complicated than the other option, but it is more reliable and does not require any additional permissions.
framehelper.html
<script src="framehelper.js"></script>
framehelper.js
var parentOrigin = location.ancestorOrigins[location.ancestorOrigins.length - 1];
if (parentOrigin === location.origin) {
// Only send a message if the frame was opened by ourselves.
chrome.runtime.sendMessage(location.hash.slice(1));
}
Code to be run in your extension page:
chrome.runtime.onMessage.addListener(frameMessageListener);
var randomMessage = 'Random message: ' + Math.random();
var f = document.createElement('iframe');
f.src = chrome.runtime.getURL('framehelper.html') + '#' + randomMessage;
document.body.appendChild(f);
function frameMessageListener(msg, sender) {
if (msg !== randomMessage) return;
var tabId = sender.tab.id;
var frameId = sender.frameId;
chrome.runtime.onMessage.removeListener(frameMessageListener);
// Note: This will cause the script to be run on the first load.
// If the frame redirects elsewhere, then the injection can seemingly fail.
f.addEventListener('load', function onload() {
f.removeEventListener('load', onload);
injectInFrame(tabId, frameId);
});
f.src = 'https://example.com';
}
Is there a way to load a page, hidden from the user?
I can't use an iframe in a background page, because the page has frame-busting techniques.
I can't use an XHR, because the page has AJAX - I need its (dynamically generated) DOM.
I'm afraid that you don't have any other option than inserting the iframe anyway. To bust the iframe buster, you can employ the following techniques:
If the iframe is blocked by the X-Frames-Option: DENY, just remove the header using the webRequest API - see Getting around X-Frame-Options DENY in a Chrome extension?.
If the frame buster uses something like
if (top !== self) {
top.location.href = location.href;
}
Then block the scripted navigation by set the sandbox attribute on the iframe:
var frame = document.createElement('iframe');
frame.sandbox = 'allow-scripts';
frame.src = 'data:text/html,<script>' +
'if (top !== self) { top.location.href = location.href;}' +
'alert(" (runs the rest of the code) ");' +
'</script>';
document.body.appendChild(frame);
Navigation will be blocked without throwing any errors. The following message is logged to the console though:
Unsafe JavaScript attempt to initiate navigation for frame with URL '(...URL of top page...)' from frame with URL '(....URL of frame..)'. The frame attempting navigation of the top-level window is sandboxed, but the 'allow-top-navigation' flag is not set.
These methods will always work, unless:
The page contains <meta http-equiv="X-Frame-Options" content="deny">.
The frame busting script is implemented as if (top === self) { /* run code*/ }
In these cases, you have no other option than opening a new tab, read its content, then close it. See chrome.tabs.create and chrome.tabs.remove.
You can use popUnder s to load data:
var win2;
function loadPopUnder(){
win2 = window.open("about:blank","",
width=150,height=150,scrollbars=0,resizable=0,toolbar=0,location=0,menubar=0,status=0,directories=0");
win2.blur();
window.focus()
}
win2.document.location.href='http://www.exampe.com/url';
Actually they may open in a new tab in certain circumstances - you have to check the actual behavior.
Also it is an advantage that this one is a browser independent solution.
Using an extension, I'd like to measure the offset of an iframe (relative to the top window) when a specific event happens in it.
This includes iframes of different origins.
It seems to require two things:
catching the event in the iframe's own content-document
finding the iframe's corresponding DOM element (<iframe>) in its parent window and measuring its offset
I can run a content-script in all iframes which would catch the specific event, but I fail to identify an iframe's corresponding DOM element in iframes with different origins, since in those cases I'm denied access to 'window.frameElement'.
Any ideas?
I could perhaps look for an element with an 'src' attribute corresponding to 'window.location.href' as seen in the context of the iframe's content-script, but 'window.location.href' wouldn't be a unique identifier... or would it?
You could take the following approach:
Use the "all_frames":true flag of "content_scripts" in the manifest file to execute the content script in all frames.
Bind a message event listener which performs the logic.
Bind an event listener for the trigger in question.
Whenever the event in step 3 is fired, use if (top === window) to determine whether the we're inside the top window.
If yes, we're done.
If not, use parent.postMessage({currentTotal: ... }, '*'); to pass data to the parent frame. The message event listener from step two should detect this event, process it, and repeat step three.
I've explained how to approach the problem. One more useful code snippet: You need to identify the source frame. To achieve that, we use the contentWindow property of an <iframe> object for comparison with the source property of the event object:
window.addEventListener('message', function(event) {
if (top === window) {
// do something with e.data
} else {
var iframes = document.getElementsByTagName('iframe');
for (var i=0; i<iframes.length; i++) {
if (iframes[i].contentWindow === event.source) {
// Found frame; measure offset, and pass data to parent frame
var data = e.data;
e.data += iframes[i].getBoundingClientRect().top; // example
parent.postMessage({currentTotal: data}, '*');
break;
}
}
}
});
With these hints you should be able to write the full code yourself. To measure offsets, use the getBoundingClientRect method. For completeness, I also include links to other relevant documentation: window.postMessage, Content scripts (manifest).
In Google Chrome's extension developer section, it says
The HTML pages inside an extension
have complete access to each other's
DOMs, and they can invoke functions on
each other. ... The popup's contents
are a web page defined by an HTML file
(popup.html). The popup doesn't need
to duplicate code that's in the
background page (background.html)
because the popup can invoke functions
on the background page
I've loaded and tested jQuery, and can access DOM elements in background.html with jQuery, but I cannot figure out how to get access to DOM elements in popup.html from background.html.
can you discuss why you would want to do that? A background page is a page that lives forever for the life time of your extension. While the popup page only lives when you click on the popup.
In my opinion, it should be refactored the other way around, your popup should request something from the background page. You just do this in the popup to access the background page:
chrome.extension.getBackgroundPage()
But if you insist, you can use simple communication with extension pages with sendRequest() and onRequest. Perhaps you can use chrome.extension.getViews
I understand why you want to do this as I have run into the problem myself.
The easiest thing I could think of was using Google's method of a callback - the sendRequest and onRequest methods work as well, but I find them to be clunky and less straightforward.
Popup.js
chrome.extension.getBackgroundPage().doMethod(function(params)
{
// Work with modified params
// Use local variables
});
Background.html
function doMethod(callback)
{
if(callback)
{
// Create/modify params if needed
var params;
// Invoke the callback
callback(params);
}
}
As other answers mention, you can call background.js functions from popup.js like so:
var _background = chrome.extension.getBackgroundPage();
_background.backgroundJsFunction();
But to access popup.js or popup.html from background.js, you're supposed to use the messages architecture like so:
// in background.js
chrome.runtime.sendMessage( { property: value } );
// in popup.js
chrome.runtime.onMessage.addListener(handleBackgroundMessages);
function handleBackgroundMessages(message)
{
if (message.property === value)
// do stuff
}
However, it seems that you can synchronously access popup.js from background.js, just like you can synchronously access the other way around. chrome.extension.getViews can get you the popup window object, and you can use that to call functions, access variables, and access the DOM.
var _popup = chrome.extension.getViews( { type: 'popup' } )[0];
_popup.popupJsFunction();
_popup.document.getElementById('element');
_popup.document.title = 'poop'
Note that getViews() will return [] if the popup is not open, so you have to handle that.
I'm not sure why no one else mentioned this. Perhaps there's some pitfalls or bad practices to this that I've overlooked? But in my limited testing in my own extension, it seems to work.