8

I am trying to select the email field in this iframe booking form. I eventually want to do something else with the field but for now as a test I just want to select the element and change the placeholder.

Getting this error so I am not selecting it correctly: Uncaught TypeError: Cannot set property 'placeholder' of null at HTMLButtonElement.changeCopy

You can view a live version of my code here and see the error in console when you click the button at the top: https://finnpegler.github.io/cart_recover/

I have also included the code as a snippet below but it throws a different error to do with cross origin frames.

var iframe = document.getElementById("booking-widget-iframe");
var field = iframe.contentWindow.document.querySelector("booking[email]");

function changeCopy() {
field.placeholder = "hello";
}

document.getElementById("button").addEventListener("click", changeCopy)
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test site for Cart Recover Tool</title>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" media="screen" href="stylesheet.css" />
<link href="https://fonts.googleapis.com/css?family=Bree+Serif|Open+Sans&display=swap" rel="stylesheet">
<link rel="icon" href="favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">  
</head>
<body>
Clicking this button should change the placeholder text in the email field below
<button id = "button">Click</button>
</body>

<script src="https://bettercleans.launch27.com/jsbundle"></script><iframe id="booking-widget-iframe" src="https://bettercleans.launch27.com/?w_cleaning" style="border:none;width:100%;min-height:2739px;overflow:hidden" scrolling="no"></iframe>
<script src="script.js"></script>
FinnPegler
  • 61
  • 6
  • 1
    Browsers will block attempts by a parent frame to manipulate the contents or structure of a child frame, if they are not of same origin, i.e. not the same domain. In your case, the parent frame origin is finnpegler.github.io but the child frame origin is bettercleans.launch27.com. If you have access to js source code of bettercleans.launch27.com, you can try and work around it by posting a message to the child frame. See if this SO post helps you https://stackoverflow.com/questions/25098021/securityerror-blocked-a-frame-with-origin-from-accessing-a-cross-origin-frame – Nivi M Jan 19 '20 at 20:38

2 Answers2

6

The reason why field is null is because when the variable was set, the value was blocked. Try opening your JavaScript console on the page and paste in

var iframe = document.getElementById("booking-widget-iframe");

Like in the source, which should work, but then see what happens when you enter in:

var field = iframe.contentWindow.document.querySelector("booking[email]");

You should get something like the following error:

Uncaught DOMException: Blocked a frame with origin "https://finnpegler.github.io" from accessing a cross-origin frame.
    at <anonymous>:1:34

So the problem isn't with field.placeholder = "hello"; being null, it's that field was never set because it was blocked from a cross-origin source.

The way to fix this: If you have access to https://bettercleans.launch27.com/?w_cleaning&iframe_id=j8szoz0b4, then somewhere on the source of that page, enter the following script:

<script>
addEventListener("message", function(e) {
    console.log(e.data);
    if(e.data["setIt"]) {
        document.querySelector("booking[email]").placeholder = e.data["setIt"];
         e.source.postMessage({
             hi:"I did it" 
         });
    }

});


</script>

Then on your https://finnpegler.github.io/cart_recover/ page source somewhere, enter the code:

<script>
var iframe;
addEventListener("load", function() {
    iframe = document.getElementById("booking-widget-iframe");
    iframe.contentWindow.postMessage({
        setIt: "hello"
    });

});
addEventListener("message", function(e) {
    console.log(e);
})
</script>

Warning: untested code, but here's the basic idea: with client side JavaScript, you can't directly edit the elements of an iframe, so the way to communicate with it is to send a message to the iframe using iframe.contentWindow.postMessage. On the iframe side, you can read that message that is coming in with the event listener "message" (or you can do window.onmessage). Then you can send JSON data or text as a message to the iframe, and read it with e.data. So in the above (untested) example, when the main github page wants to change the value of the placeholder to a specific amount, it simply sends a JSON object containing the placeholder data to the iframe, and then on the iframe source side, it reads that incoming message, checks the JSON if it has the set key for setting the placeholder, and if it has that key, then set the value of the placeholder to the value of that property.

Let me know if you have any more questions.

Here's some references:

https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage https://blog.teamtreehouse.com/cross-domain-messaging-with-postmessage https://javascript.info/cross-window-communication https://bl.ocks.org/pbojinov/8965299 https://www.ilearnjavascript.com/plainjs-postmessage-and-iframes/ https://medium.com/@Farzad_YZ/cross-domain-iframe-parent-communication-403912fff692

bluejayke
  • 2,773
  • 2
  • 20
  • 50
0

First let's bring some clarity into what's really happening. To aid the analysis, I've modified your script without introducing any side effect as follows:

var iframe = document.getElementById( "booking-widget-iframe" );

console.log( `iframe: ${ iframe }` );
console.log( `iframe.contentWindow: ${ iframe.contentWindow }` );
console.log( `iframe.contentWindow.document: ${ iframe.contentWindow.document }` );
console.log( `iframe.contentWindow.document.querySelector("booking[email]"): ${ iframe.contentWindow.document.querySelector( "booking[email]" ) }` );

function changeCopy() {
    console.log( "-------------------------------------------------------------------------------" );
    console.log( "changeCopy: started" );
    console.log( "-------------------------------------------------------------------------------" );

    var iframe = document.getElementById( "booking-widget-iframe" );

    console.log( `iframe: ${ iframe }` );
    console.log( `iframe.contentWindow: ${ iframe.contentWindow }` );
    console.log( `iframe.contentWindow.document: ${ iframe.contentWindow.document }` );
    console.log( `iframe.contentWindow.document.querySelector("booking[email]"): ${ iframe.contentWindow.document.querySelector( "booking[email]" ) }` );

    var field = iframe.contentWindow.document.querySelector( "booking[email]" );
    field.placeholder = "hello";

    console.log( "-------------------------------------------------------------------------------" );
    console.log( "changeCopy: finished" );
    console.log( "-------------------------------------------------------------------------------" );
}

document.getElementById( "button" ).addEventListener( "click", changeCopy )

Loading the document with the above script proved the following outcome: Run output of script

What have we learned from the output?

  1. Before the button was clicked, iframe.contentWindow.document.querySelector( "booking[email]" ) evaluates to null. Therefore variable field was essentially null.

  2. Why did iframe.contentWindow.document.querySelector( "booking[email]" ) evaluate to null before the button click?

    • Since the iframe's document had not loaded, the browser (Safari) considered it suboptimal to run security policies between the parent and child frames.
  3. By the time I pressed on the Click button, the document of #booking-widget-iframe (child frame) had loaded completely, and enforcement of security policies between the parent and child frames was enabled. Consequently, the attempt to access iframe.contentWindow was promptly blocked due to failed compliance with the cross-origin frame policy.

Possible resolutions:

  • In order for scripts in the parent frame to be allowed access to objects in the child frame, both parent and child frames should be loaded from the same domain (i.e. https://bettercleans.launch27.com) and port (irrelevant for the case in consideration).
  • Alternatively, make use of post messaging for interaction between both frames as documented in Window.postMessage().

Assuming you could apply the resolution, there is another issue that should pop up . iframe.contentWindow.document.querySelector("booking[email]") would yet be null. The root cause of the subsequent issue would be due to no element named booking could be found in the targeted document. In other words, the selector "booking[email]" is erroneous.

Igwe Kalu
  • 12,070
  • 2
  • 23
  • 37