Magento 2 Page Cache and Foreign Iframe Support

One of Magento 2 goals is to enable performance for all and scalability for Enterprise customers. Page Cache plays an important role in Magento 2. Starting from Magento 2.0.0 release Page Cache functionality (it is also known as Magento Full Page Cache) is available for Magento 2 Community Edition.

I have a chance to work with Google AdWords Dynamic Remarketing functionality. It appears Page Cache has some glitch issue when external iframe is loaded on a page. In this article I want to share my experience with adding foreign iframe support for Magento 2.

Page Cache Background

Page Cache functionality allows to cache full pages content. All requests and responses in the platform are passed through Magento 2 plugins. As for Magento 2.0.4 version Magento\PageCache supports 2 types of cache providers: Built In and Varnish.

With the “Built In” support Magento 2 Developer may easily work on local environment and observe techniques of Page Caching functionality. This type is not recommended for production environments.

Varnish cache type allows to store cache response directly into Varnish server. Magento\PageCache module comes with Varnish 3 and 4 VCL files ready to be used.

Page Cache configuration is available for Merchant under Magneto Admin -> Store -> Configuration -> System -> Full Page Cache section as shown below:

Magento 2 Page Cache

As you may see VCL files might be configured with settings provided by a Merchant via admin panel. Later the VCL file is used to configure Varnish server. I think Full Page Cache functionality is a good topic for my next article. In this one I am going to share something interesting.

The page-cache.js Component

The Magento\PageCache\view\frontend\web\js\page-cache.js component is responsible for handling user private content.
User Private Content stays for all sensitive user’s information and state. As example of USP might be Shopping Cart, Wish List, Recently Viewed Products, Login/Logout etc.

The page-cache.js component provides 2 jQuery Widgets. First one is FormKey Widget responsible for generation of new Form Key value and storing it in User’s cookie storage. Second one is pageCache Widget responsible for collecting special HTML placeholders in order to update private content asynchronously using AJAX request.

Let’s see how pageCache Widget _create method looks like:

_create: function () {
    var placeholders,
        version = $.mage.cookies.get(this.options.versionCookieName);

    if (!version) {
        return;
    }
    placeholders = this._searchPlaceholders(this.element.comments());

    if (placeholders && placeholders.length) {
        this._ajax(placeholders, version);
    }
},

An important role in the _create method plays comments function which collects all BLOCKcomments from HTML source.

placeholders = this._searchPlaceholders(this.element.comments());

Here is comments function implementation:

/**
     * Nodes tree to flat list converter
     * @returns {Array}
     */
    $.fn.comments = function () {
        var elements = [];

        /**
         * @param {jQuery} element - Comment holder
         */
        (function lookup(element) {
            $(element).contents().each(function (index, el) {
                switch (el.nodeType) {
                    case 1: // ELEMENT_NODE
                        lookup(el);
                        break;

                    case 8: // COMMENT_NODE
                        elements.push(el);
                        break;

                    case 9: // DOCUMENT_NODE
                        var hostName = window.location.hostname,
                            iFrameHostName = $('<a>')
                                .prop('href', element.prop('src'))
                                .prop('hostname');

                        if (hostName === iFrameHostName) {
                            lookup($(el).find('body'));
                        }
                        break;
                }
            });
        })(this);

        return elements;
    };

It goes through all DOM nodes and finds all comment nodes represented as nodeType = 8.

The comments function uses jQuery contents() method. This method reads all child nodes of \node recursively and returns these elements.

Finally

There is a limitation with comments() function in case one of Magento 2 pages contain foreign iframe.
jQuery contents() method checks whether node name is iframe and if yes uses contentDocument property of DOM node.

contents: function( elem ) {
    return jQuery.nodeName( elem, "iframe" ) ?
    elem.contentDocument || elem.contentWindow.document :
    jQuery.merge( [], elem.childNodes );
}

During implementation of the Google AdWords Dynamic Remarketing feature I use https://www.googleadservices.com/pagead/conversion.js script which loads foreign iframe to a website.

Here is a function in the conversion.js script responsible for URL preparation:

function Z(a, b, d) {
        d = d.google_remarketing_only ? "googleads.g.doubleclick.net" : d.google_conversion_domain || "www.googleadservices.com";
        return Y(a) + "//" + d + "/pagead/" + b
    }

jQuery contents() method throws an error when src attribute of iframe node is an external source:


jquery.js:2998 Uncaught SecurityError: Failed to read the ‘contentDocument’ property from ‘HTMLIFrameElement’

It blocked a frame with origin “http://demo.magento2.com” from accessing a frame with origin “https://www.google.ie”. The frame requesting access has a protocol of “http”, the frame being accessed has a protocol of “https”. Protocols must match.

How to fix

In order to fix enable foreign iframe support and load Google related content the page-cache.jscomponent should check whether an iframe loaded on a page has same hostname before jQuery contents() method is called.

if ($.nodeName(element, "iframe") && $(element).prop('src').indexOf(window.location.hostname) != -1) {
    return;
}

Updated comments() function might look like as follow:

$.fn.comments = function () {
    var elements = [];

    /**
     * @param {jQuery} element - Comment holder
     */
    (function lookup(element) {
        if ($.nodeName(element, "iframe") && $(element).prop('src').indexOf(window.location.hostname) != -1) {
            return;
        }
        $(element).contents().each(function (index, el) {
            switch (el.nodeType) {
                case 1: // ELEMENT_NODE
                    lookup(el);
                    break;

                case 8: // COMMENT_NODE
                    elements.push(el);
                    break;

                case 9: // DOCUMENT_NODE
                    var hostName = window.location.hostname,
                        iFrameHostName = $('<a>')
                            .prop('href', element.prop('src'))
                            .prop('hostname');

                    if (hostName === iFrameHostName) {
                        lookup($(el).find('body'));
                    }
                    break;
            }
        });
    })(this);

    return elements;
};

Once it’s done Google iframe might be easily loaded and there should not be any errors because of same origin policy violation.

Summary

Page Cache functionality should be updated and read only origin document related to DOM nodes. It allows to integrate marketing features such as Google AdWords Dynamic Remarketing without any errors.

Page Cache functionality should be updated and read only origin document related to DOM nodes. It allows to integrate marketing features such as Google AdWords Dynamic Remarketing without any errors.

This post was written for Atwix blog by Max Pronko (CTO at GiftsDirect and TheIrishStore.com).