Committer: dpetrov
LJSUP-11155: Dynamic iframe for tackthis tagA trunk/htdocs/js/lj.postmessage.js A trunk/htdocs/js/lj.rpc.js U trunk/htdocs/misc/xdr.html
Added: trunk/htdocs/js/lj.postmessage.js =================================================================== --- trunk/htdocs/js/lj.postmessage.js (rev 0) +++ trunk/htdocs/js/lj.postmessage.js 2012-02-09 14:06:53 UTC (rev 11460) @@ -0,0 +1,463 @@ +/*! + * LiveJournal crossdomain messaging plugin plugin. + * @author Dmitry Petrov <Dmitry.Petrov@sup.com>, 2011-2012 + * + * Plugin is used to allow crossdomain communication between container window and child + * popups or frames. To make communication work in older browsers, user has to put html + * (xdr.html) file in the root of container page domain. + * + * To allow communication either container should initiate communication or child popup + * should know the origin of the container ( protocol://host:port ). + * + * Contents of xdr.html: + * <!DOCTYPE HTML> + * <html lang="en"> + * <head> + * <meta charset="UTF-8"> + * <script type="text/javascript"> + * function sendData() { + * if ( location.hash && location.hash.length > 1 ) { + * window.parent.opener.rpcMessage = location.hash; + * location.hash = '#'; + * } + * } + * + * setInterval( sendData, 50 ); + * </script> + * </head> + * <body> + * </body> + * </html> + * + * Plugin was tested to work in IE7+, Firefox 3.5+, Opera 10.10+, Google Chrome 12+ + * + * @TODO: Plugin doesn't check origin of the messages for now, though it's not possible for old + * browser because of technology used to pass data ( hash polling ). + * + */ +(function( window, $ ){ + var cache_bust = 1, + interval_id, + + /** + * window name is used to identify child window from container side. + */ + windowId = window.name, + + /** + * has_postMessage shows if browser supports postMessage communication. IE8 is disabled because it + * doesn't allow to use postMessage from popup. + * For frames, postMessage should be enabled from frames. + */ + isIE8 = $.browser.msie && parseInt( $.browser.version, 10 ) === 8, + isPopup = !!window.opener, + has_postMessage = !!window.postMessage && ( !$.browser.msie || parseInt( $.browser.version, 10 ) > 8 ), + + // isIE8 = true, + // isPopup = !!window.opener, + // has_postMessage = false, + /** + * senderUrl stores the address of container window + */ + senderUrl, + /** + * iframeUrl stores the address of iframe in the domain of container window + */ + iframeUrl, + receiveCallbacks = [], + senderDomainFrame, + /* + * pollDelay is a delay for checking it hash changed + */ + pollDelay = 100, + /* + * Buffer stores messages that could no be delivered because the url of parent window + * was not set. Array is used only in cases when the fallback method is used instead + * of postMessage. + */ + unsentMessages = []; + + /** + * targets is a collection of targets to exhange messages with. Container can pass messages to the multiple domains + */ + var targets = function() { + var container = []; + + var collection = { + add: function( w, ref, isFrame ) { + var pos; + if( ( pos = collection.indexOf( w ) === -1 ) ){ + container.push( { win: w, href: ref, isFrame: isFrame } ); + } else { + collection[ pos ].href = ref; + collection[ pos ].isFrame = isFrame; + } + }, + + remove: function( w ) { + var pos; + if( ( pos = collection( w ) === -1 ) ){ + return; + } else { + collection.splice( pos, 1 ); + collection[ pos ].href = ref; + } + }, + + indexOf: function( w ) { + for( var i = 0; i < collection.length; ++i ) { + if( w === collection[ i ].top ) { + return i; + } + } + + return -1; + }, + + getTarget: function( win ) { + var pos = collection.indexOf( win ); + if( pos === -1 ) { + return; + } else { + return container[ pos ]; + } + } + }; + + return collection; + }(); + + function createPacket(message) { + // packet = typeof message === 'string' ? { data: message } : message + return { frameId: windowId, message: message }; + } + + /** + * Converts message object to the hash string ready to pass to other window + * + * @param {Object} message + * @return {String} + */ + function toHash( message ) { + var params = jQuery.param(createPacket(message)); + return '#' + (+new Date) + (cache_bust++) + ':rpc&' + params; + } + + /** + * Converts a string that was serialized with jQuery.param back to the object. + * + * @param {String} str + * + * @return {Object} + */ + function toObject(str) { + var obj = {}, pair; + var pairs = decodeURIComponent(str).split( "&" ); + var injectParam = function(key, val) { + var firstBracket = key.indexOf('['); + + if (firstBracket === -1) { + obj[key] = val; + return; + } + + var prevkey = key.substring(0, firstBracket), + key = key.substr(firstBracket), + prev = obj, + newobj, + newkey; + + key.replace(/\[([^\]]+)?\]/g, function(chunk, idx, pos) { + var newobj, newkey; + if (chunk.match(/\[\d*\]/)) { + newobj = prev[prevkey] || []; + newkey = idx || '[]'; + } else { + newobj = prev[prevkey] || {}; + newkey = idx; + } + + if (prevkey === '[]') { + prev.push(newobj); + } else { + prev[prevkey] = newobj; + } + + prev = newobj; + prevkey = newkey; + }); + + if (prevkey === '[]') { + prev.push(val); + } else { + prev[prevkey] = val; + } + } + + //if user passes simple string, we return it as an response + if (pairs.length === 1) { + return pair[0]; + } + + for( var arg = 0; arg < pairs.length; arg++ ) { + pair = pairs[ arg ].split( "=" ); + injectParam(pair[0], pair[1]); + } + + return obj; + } + + /** + * Checks if hash contains correct rpc call + */ + function isHash( str ) { + return /#\d{14}:rpc(.*)&/.test( str ); + } + + function extractBaseUrl( url ) { + return url.replace( /([^:]+:\/\/[^\/]+).*/, '$1' ) + } + + /** + * Function tries to find rpc call through different communication channels ( hash or window rpcMessage property ) + * + * @return {Object} return + */ + function catchEvent() { + var channel = ''; + if( !has_postMessage && location.hash.length > 0 && /^#\d+:rpc&.*$/.test( location.hash ) ) { + channel = location.hash; + location.hash = ''; + } else if( window.rpcMessage && window.rpcMessage.length > 0 && /^#\d+:rpc&.*$/.test( window.rpcMessage ) ) { + channel = window.rpcMessage; + window.rpcMessage = ''; + } else { + return; + } + + return toObject( channel.replace( /#\d+:rpc&/, '' ) ); + } + + function receiveMessage(data) { + for( var i = 0; i < receiveCallbacks.length; ++i ) { + var cb = receiveCallbacks[ i ].pmcb || receiveCallbacks[ i ].cb; + cb( { data: data } ); + } + } + + /** + * Polling loop + */ + function startPolling() { + interval_id && clearInterval( interval_id ); + interval_id = null; + + interval_id = setInterval( function() { + var event = catchEvent(), + message = event && event.message; + + if( event ) { + if (message && message.name === 'rpc.batch' && + message.data && message.data.length > 0) { + for (var i=0; i < message.data.length; ++i ) { + receiveMessage(message.data[i]); + } + } else { + receiveMessage(event); + } + } + }, pollDelay ); + } + + /** + * @namespace LJ.rpc + */ + window.LJ = window.LJ || {}; + LJ.rpc = LJ.rpc || {}; + + /** + * Send message to the other window. If senderUrl us set the code acts like child window. + * + * @param {Object|String} message + * @param {Object} target Window object of the window to send message to + */ + LJ.rpc.postMessage = function( message, target ) { + target = target || parent; + + var packet = createPacket(message), + target_url, + useFrame = false, + targetInfo = targets.getTarget(target), + //all this is done to prevent the usage of postMessage transport + //between container and popup in IE8 + usePM = has_postMessage || (isIE8 && ( + (targetInfo && targetInfo.isFrame) || + ((target === parent) && !isPopup))); + + target_url = targetInfo && target.href; + + if (!(targetInfo)) { + if(usePM) { + target_url = "*"; + } else { + if( !senderUrl ) { + // throw "Transport channel has not been init for communication"; + //all these messages will be sent as soon as sender will be set + unsentMessages.push(jQuery.extend(true, {}, packet)); + } else { + target_url = senderUrl; + useFrame = true; + } + } + } + + if (usePM) { + window.setTimeout( function() { + if (isIE8) { + packet = jQuery.param(packet); + } + target.postMessage(packet, extractBaseUrl( target_url ) ); + }, 0 ); + + } else if ( target_url ) { + if( useFrame ) { + // window.open( iframeUrl + toHash( message ), 'xdr' ); + $( senderDomainFrame ).attr( 'src', iframeUrl + toHash( message ) ); + } else { + target.location = target_url.replace( /#.*$/, '' ) + toHash( message ); + } + } + }; + + /** + * Bind callback to listen messages + * + * @param {Function} callback + */ + LJ.rpc.bind = function( callback ) { + if ( has_postMessage || isIE8 ) { + var rm_callback = function(e) { + e = e || window.event; + if (isIE8) { + try { + e = { data: toObject(e.data) }; + } catch(err) {}; + } + callback( e ); + }; + + if ( window.addEventListener ) { + window.addEventListener( 'message', rm_callback, false ); + } else { + window.attachEvent( 'onmessage', rm_callback ); + } + receiveCallbacks.push( { cb: callback, pmcb: rm_callback } ); + } else { + receiveCallbacks.push( { cb: callback } ); + } + }; + + /** + * Stop listening messages with callback + */ + LJ.rpc.unbind = function( callback ) { + for( var i = 0; i < receiveCallbacks.length; ++i ) { + if( receiveCallbacks[ i ].cb === callback ) { + if ( has_postMessage ) { + if ( window.addEventListener ) { + window.removeEventListener( 'message', receiveCallbacks[ i ] ); + } else { + window.detachEvent( 'onmessage', receiveCallbacks[ i ] ); + } + } + receiveCallbacks.splice( i, 1 ); + return; + } + } + } + + /** + * Init communication with child window from container. Function makes sense only for browsers + * that do not support postMessage. + * + * @param {Object} w window object of child window. + * @param {String} recipientUrl url if the child window + * @param {String} senderUrl url of the container window + * @param {boolean=} isFrame with this flag equal to true user can explictly show that recipient is frame. + * In this case IE8 will use postMessage transport. Default is false + */ + LJ.rpc.initRecipient = function( w, recipientUrl, senderUrl, isFrame ) { + //if only fallback method is used, we have to pass parent window url with query string to enable communication + if( !(has_postMessage || (isIE8 && isFrame))) { + w.location = recipientUrl.replace( /#.*$/, '' ) + toHash( { transport_url: senderUrl } ); + } else if (!isFrame) { + w.location = recipientUrl; + } + + targets.add( w, recipientUrl, !!isFrame ); + }; + + //plugin listens messages in case if parent window will send it's url. + if( !has_postMessage ) { + var bootstrap = function( e ) { + if( e.data && e.data.message && e.data.message.transport_url ) { + LJ.rpc.setSender( e.data.message.transport_url ); + + if (unsentMessages.length > 0) { + LJ.rpc.postMessage({ + name: 'rpc.batch', + data: unsentMessages + }); + + unsentMessages.length = 0; + } + + LJ.rpc.unbind( bootstrap ); + } + }; + LJ.rpc.bind( bootstrap ); + } + + /** + * Set Url of the parent window in the child. Function also initiates communication channel, + * if it has not been done yet. + * + * @param {String} url + */ + LJ.rpc.setSender = function( url ) { + senderUrl = url; + + //setting senderUrl means that we're in child window, so we need + //to create the page in the domain of parent window + if( !has_postMessage ) { + iframeUrl = extractBaseUrl( url ) + '/xdr.html'; + + if( !senderDomainFrame ) { + senderDomainFrame = document.createElement( 'iframe' ); + senderDomainFrame.id = 'xdr'; + senderDomainFrame.src = iframeUrl; + senderDomainFrame.style.display = 'none'; + + document.body.insertBefore( senderDomainFrame, document.body.firstChild ); + } else { + $( senderDomainFrame ).attr( 'src', iframeUrl ); + } + + } + }; + + /** + * Return current url of the parent window. + * + * @return {String} + */ + LJ.rpc.getSender = function() { + return senderUrl; + }; + + LJ.rpc.hasPostMessage = has_postMessage; + + startPolling(); + +})( window, jQuery ); + Added: trunk/htdocs/js/lj.rpc.js =================================================================== --- trunk/htdocs/js/lj.rpc.js (rev 0) +++ trunk/htdocs/js/lj.rpc.js 2012-02-09 14:06:53 UTC (rev 11460) @@ -0,0 +1,56 @@ +/** + * This file represent fuctionality that livejournal exposes to the + * iframes from third-party domains. + */ +(function($) { + var getFrame = function(name) { + var frame = $('#' + name); + + return frame.length === 0 ? null : frame; + }; + + var rpc = { + adjustHeight: function(frameId, height) { + var MAX_HEIGHT = 3000; + if (height < 0) { return; } + if (height > MAX_HEIGHT) { height = MAX_HEIGHT; } + + var frame = getFrame(frameId); + + if (frame) { + frame.height(height); + } + } + }; + + var handlePostMessage = function(ev) { + var frameId = ev.data.frameId, + message = ev.data.message, + func; + + if (frameId && message && message.name && message.name.match(/^rpc\./)) { + func = message.name.substr(4); + + if (rpc.hasOwnProperty(func)) { + rpc[func].apply(null, [frameId].concat(message.data)); + } + } + }, + init = function() { + if(LJ.rpc) { + LJ.rpc.bind(handlePostMessage); + } else { + setTimeout(init, 10); + } + }, + initFrames = function() { + $('iframe.rpc').each(function() { + LJ.rpc.initRecipient(this.contentWindow, + this.getAttribute('src'), location.href.replace( /#.*$/, '' ), true); + }); + }; + + init(); + jQuery(initFrames); +})(jQuery); + Modified: trunk/htdocs/misc/xdr.html =================================================================== --- trunk/htdocs/misc/xdr.html 2012-02-09 14:04:30 UTC (rev 11459) +++ trunk/htdocs/misc/xdr.html 2012-02-09 14:06:53 UTC (rev 11460) @@ -3,9 +3,10 @@ <head> <meta charset="UTF-8"> <script type="text/javascript"> + var parentWin = window.parent.opener || window.parent.parent; //frame or popup function sendData() { if ( location.hash && location.hash.length > 1 ) { - window.parent.opener.rpcMessage = location.hash; + parentWin.rpcMessage = location.hash; location.hash = '#'; } }