Last Updated: February 25, 2016
·
3.431K
· kfwerf

CORS Iframe messaging with DOM access and callback

Sometimes you can't go around using iframes, but you still want your own design and have the iframe do your commands but be invisible. I made a small little utility that uses the postmessage to send and receive json parsed DOM data.

Now i haven't set this all up to be 100% safe yet, it's origin calls are wildcarded which on production would be ill advised. Anyway it works and you could send flow through the iframe and pick off errors by checking at which page it is or what the dom is doing. It's not pretty but iframes aren't pretty..

Place this on the iframe side to enable some dom manipulation via postmessage packages

#   DOM Manipulation via PostMessage
#
do ( dom = window ) ->

    #
    #   Abstract API for doing small DOM manipulations
    #
    class DomManipulator
        parseHTML: ( strHtml ) ->
            elConverted = document.implementation.createHTMLDocument()
            elConverted.body.innerHTML = strHTML
            elConverted.body.children
        stringifyHTML: ( elTarget ) ->
            if typeof elTarget is 'object' then elTarget.outerHTML else false
        getElement: ( strElement ) ->
            elTarget = document.body.querySelector( strElement )
            elTarget
        getText: ( strElement ) ->
            elTarget = @getElement strElement
            elTarget.textContent
        getProperty: ( strElement, strProperty ) ->
            elTarget = @getElement strElement
            if elTarget then elTarget.getAttribute( strProperty ) else false
        setProperty: ( strElement, strProperty, strValue ) ->
            elTarget = @getElement strElement
            if elTarget then  elTarget.setAttribute strProperty, strValue #elTarget[strProperty] = strValue
            @getProperty strElement, strProperty
        doMouseEvent: ( strElement, strEvent ) ->
            elTarget = @getElement strElement
            if elTarget
                evtMouseAny = document.createEvent('MouseEvents')
                evtMouseAny.initMouseEvent strEvent, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null
                elTarget.dispatchEvent evtMouseAny
                true
            false

    #
    #   Extends DOM Manipulator and transforms the manipulations to be compatible with PostMessage
    #
    class PostMessageDomManipulator extends DomManipulator
        constructor: () ->

            #
            #   Public PostMessage DomManipulator API
            #
            @objApi =
                getElement: (( objOptions, objJsonData ) -> 
                    strElement = objOptions['strElement']
                    if strElement then objJsonData.strReturn = @stringifyHTML @getElement(strElement)
                ).bind @
                getText: (( objOptions, objJsonData ) ->
                    strElement = objOptions['strElement']
                    if strElement then objJsonData.strReturn = @getText strElement
                ).bind @
                getProperty: (( objOptions, objJsonData ) ->
                    strElement = objOptions['strElement']
                    strProperty = objOptions['strProperty']
                    if strElement and strProperty then objJsonData.strReturn = @getProperty strElement, strProperty
                ).bind @
                setProperty: (( objOptions, objJsonData ) -> 
                    strElement = objOptions['strElement']
                    strProperty = objOptions['strProperty']
                    strValue = objOptions['strValue']
                    if strElement and strProperty and strValue then objJsonData.strReturn = @setProperty strElement, strProperty, strValue
                ).bind @
                doMouseEvent: (( objOptions, objJsonData ) ->
                    strElement = objOptions['strElement']
                    strEvent = objOptions['strEvent']
                    if strElement and strEvent then objJsonData.strReturn = @doMouseEvent strElement, strEvent
                ).bind @

            # Give a debug message that can be read
            console.debug 'Initializing postMessage protocol'

            # Bind the postMessage to onReceiveData. That will transform the call for the Public API
            dom.addEventListener 'message', @onReceiveData.bind( @ )
            @doSendData.bind(@) boolInitialized: true, strId: 'intitialize'

        #
        #   For receiving messages via PostMessage to DOM Manipulation
        #
        onReceiveData: ( e ) ->
            strOrigin = e.origin
            objJsonData = JSON.parse e.data
            strLocation = objJsonData['strLocation']
            strAction = objJsonData['strAction']
            objParameters = objJsonData['objParameters']

            # Parent always needs to say what he expects, else we might end up going out of sync
            boolExpected = strLocation is window.location.href or strLocation is '*'
            boolOrigin = true
            boolAction = strAction
            boolParameters = objParameters
            boolPassing = boolExpected and boolOrigin and boolAction and boolParameters

            if not boolPassing then return @doSendData strErrorMessage: 'Origin not allowed or you were expecting someone else.'

            # Try to execute the request by testing if the action exists and can be called
            try
                objJsonData['strReturn'] = false
                if @objApi[strAction] then @objApi[strAction]( objParameters, objJsonData )
                @doSendData objJsonData
            catch
                @doSendData { strErrorMessage: 'Could not pass action onto manipulator. Something malformed..' } 

        #
        #   For sending messages via PostMessage back to the parent
        #
        doSendData: ( objData = {} ) ->
            objData.objLocation = window.location
            objData.strId = objData.strId  or 'keep-alive'
            strOrgin = '*' # Needs to be set for safety, only send messages to allowed domain
            strJsonData = JSON.stringify objData
            if parent
                parent.postMessage strJsonData, strOrgin
    #
    #   on DOMContentLoaded initialize the PostMessageDomManipulator
    #
    onLoad = () ->
        dom.pmdm = new PostMessageDomManipulator()
    dom.addEventListener 'DOMContentLoaded', onLoad

On the parent place something like

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>

    <script>
        window.onload = function() {

            var onClickDemo = function() {
                console.info('Trying to post a message to the target');
                var objData = {
                    strId: '',
                    strLocation: '*',
                    strAction: 'setProperty',
                    objParameters: {
                        strElement: '#amount',
                        strProperty: 'value',
                        strValue: '100'
                    }
                }

                document.getElementsByTagName('iframe')[0].contentWindow.postMessage(JSON.stringify(objData), '*');
            },
            onClickTriggerDemo = function() {
                console.info('Trying to trigger the mouse event');

                var objData = {
                    strId: 'ontoNextPage',
                    strLocation: '*',
                    strAction: 'doMouseEvent',
                    objParameters: {
                        strElement: '#continueButton',
                        strEvent: 'click'
                    }
                }
                document.getElementsByTagName('iframe')[0].contentWindow.postMessage(JSON.stringify(objData), '*');
            },
            onGetErrorMessage = function() {
                console.info('Trying to get the error');

                var objData = {
                    strLocation: '*',
                    strAction: 'getElement',
                    objParameters: {
                        strElement: '#textWithdrawalDeclined'
                    }
                }
                document.getElementsByTagName('iframe')[0].contentWindow.postMessage(JSON.stringify(objData), '*');
            }
            onMessageReceived = function( e ) {
                console.debug( 'data;', JSON.parse(e.data));
            }
            elIframe = document.createElement('iframe');
            elIframe.width = '100%';
            elIframe.height = '800';

            document.body.querySelectorAll( '.try' )[0].addEventListener('click', onClickDemo);
            document.body.querySelectorAll( '.do' )[0].addEventListener('click', onClickTriggerDemo);

            window.addEventListener( 'message', onMessageReceived )
            document.body.appendChild(elIframe);
        }
    </script>

    <input type="submit" value="Try a postMessage" class="try">
    <input type="submit" value="Try a trigger" class="do">
</body>
</html>

Basically you throw packages over the line JSON stringified:

var objData = {
    strId: '',
    strLocation: '*',
    strAction: 'setProperty',
    objParameters: {
        strElement: '#amount',
        strProperty: 'value',
        strValue: '100'
    }
}

document.getElementsByTagName('iframe')[0].contentWindow.postMessage(JSON.stringify(objData), '*');

PostMessage DOM i have made very small, you have the following:

  • getElement
  • getText
  • getProperty
  • setProperty
  • doMouseEvent

You can check them and add to them in the DomManipulator

Cheers!