SharePoint 2013 – Integration Challenges – #5 Same Origin Policy – HTML5 postMessage

Hi,

I’m currently involved in integrating SharePoint with IBM Connections and I’m having a lot of fun trying to figure out all the possibilities. Since these integration considerations are not specific to SharePoint/IBM Connections, I’ll blog a series of posts which will be rather short or rather long according to the topic I’m focusing on.
In this post, I’m going to focus on HTML5 postMessage. The HTML5 PostMessage API is another means to allow cross-origin communication. In a nutshell this is based on iframes or windows that are used as intermediate objects to establish the communication between the parent page and the child page as shown below:

postmessage

    • 1. The parent page embeds an IFRAME that points to the child page. Both the child page and the parent page declare a onmessage handler to receive incoming messages
    • 2. The parent page sends a message, usually in JSON format to the child page using the htmlPostMessage method of the IFRAME object
    • 3. The child page potentially replies something

There are other modes based on popup windows but the concepts are similar.

Scenario

In this post, I’m going to elaborate on a scenario where a page hosted in IBM Connection consumes SharePoint data. Here is a screenshot of that page:
connectionpage
When clicking on the buttons, the page retrieves in JSON format either the list of documents (excluding pptx) authored by the current user, either the list of presentations (only pptx).

Let’s code it now

Now that you understand better the scenario, let’s see how to achieve this.

Code of the sender page

The sender page is hosted in IBM Connections and it wil send a request to the SharePoint page to receive either the documents, either the presentations.

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="jquery1-9-1.min.js"></script>        
<script type="text/javascript" src="json2.js"></script>
<script type="text/javascript">      
var ReceiverOrigin = 'http://sdww0450:38825';          
function GetSharePointData(operation) {
  SendMessage('{"OperationType":'+operation+'}');
}

window.onmessage = function (e) {
  if(event.origin != ReceiverOrigin)
  {
    console.log('origin not allowed : '+event.origin);
    return;      
  }

  if(event.data)
  {
     var results = JSON.parse(event.data);
     if(results.Message.SearchResults){
      for(var i = 0;i<results.Message.SearchResults.length;i++)
      {
       console.log(results.Message.SearchResults[i].Title + ':'+results.Message.SearchResults[i].Path);
      }
     }
  } 		
  $('#ReceivedContent').empty().append(event.data);
};                

function SendMessage(data) {
 var win = document.getElementById("iframe").contentWindow;
 win.postMessage(
   data,
   ReceiverOrigin);
   return false;
}

</script>
</head>
    <body>
     <input type="button" onclick="GetSharePointData(1)" value="Get My Recent Documents"/>
     <input type="button" onclick="GetSharePointData(2)" value="Get My Recent Presentations"/>
     <iframe id="iframe" style="display:none;" src="http://sdww0450:38825/_layouts/15/connections/receiver.html?v=3"></iframe>
<div id="ReceivedContent"></div>
</body>
</html>

So, basically, it subscribes to the onmessage event to receive incoming calls which in our situation is to handle replies from SharePoint. It’s checking the origin because messages can come from anywhere since the onmessage event is opened to the entire world :). It declares a hidden iframe that points to the SharePoint page. It declares the function that effectively performs the postMessage call. At last, the buttons call a common JavaScript function that sends the order (1 or 2) to SharePoint.
Remember that if your IFRAME targets a SharePoint page, you’ll need to get rid of the X-FRAME-OPTIONS, go read this post if needed http://www.silver-it.com/node/149

Code of the receiver page

This code is a little bit more complex but this is due to the amount of things that need to be done on the SharePoint side.

<script type="text/javascript">
    "use strict";
    (function () {
        var SPObject = null;
        var CurrentUser = null;        
        Type.registerNamespace('SilverIT.SPIntegrationDemo');
        SilverIT.SPIntegrationDemo = function () {                        
            var ctx = new SP.ClientContext.get_current();
            var web = ctx.get_web();
            ctx.load(web);
            var user = web.get_currentUser();
            user.retrieve();
            ctx.executeQueryAsync(
                function () {
                    CurrentUser = user.get_loginName().substring(user.get_loginName().lastIndexOf('|') + 1);                                        
                },
                function (data) {
                    throw "Could not retrieve current user";
                });
        };

        SilverIT.SPIntegrationDemo.prototype = (function () {
            TransformSearchResults = function (results) {
                var JsonData = '{ "SearchResults":[';
                var Paths = JSLINQ(results)
                 .Select(function (item) {
                     return JSLINQ(item.Cells.results).Where(
                         function (r) { return r.Key === 'Path' || r.Key === 'Title'; }).Select(
                         function (item) { return item.Value });
                 });
                for (var p = 0; p < Paths.items.length; p++) {                     if (p > 0)
                        JsonData += ',';
                    JsonData += '{"Title":"' + ((Paths.items[p].items[1] != '') ? Paths.items[p].items[1] : Paths.items[p].items[0]) + '",';
                    JsonData += '"Path":"' + Paths.items[p].items[0] + '"}';                    
                }
                JsonData += "]}";
                return JsonData;
            }
            return {            
                GetMyRecentDocuments: function GetMyRecentDocuments(source, origin) {                    
                    $.ajax({
                        url: "/_api/search/query?querytext='-FileExtension:\"ppt*\" AND IsDocument:1 AND AuthorOWSUser:\"" + CurrentUser + "\"'&selectproperties='Path,Title'&rowlimit=5&sortlist='Created:descending'",
                        headers: { "Accept": "application/json; odata=verbose" },
                        contentType: "application/json; odata=verbose",
                        success: function (data, textStatus) {                            
                            var results;
                            if (data.d) {                                
                                if (data.d.query)
                                    results = data.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results;
                                else if (data.d.postquery)
                                    results = data.d.postquery.PrimaryQueryResult.RelevantResults.Table.Rows.results;
                                else
                                    throw "Results not found";                                
                                if (results.length === 0) {
                                    SPObject.PostMessage(source, origin, '""', '');
                                    return;
                                }
                                else {
                                    SPObject.PostMessage(source, origin, TransformSearchResults(results), '');
                                }
                                
                            }
                        },
                        error: function (data, errorCode, errorMessage) {
                            if (data.error.message !== undefined) {
                                SPObject.PostMessage(source, origin, '""', data.error.message.value);
                            }
                            else {
                                SPObject.PostMessage(source, origin, '""', data.responseText);
                            }
                        }
                    });
                },
                GetMyRecentPresentations: function GetMyRecentPresentations(source, origin) {
                    $.ajax({
                        url: "/_api/search/query?querytext='FileExtension:\"ppt*\" AND IsDocument:1 AND AuthorOWSUser:\"" + CurrentUser + "\"'&selectproperties='Path,Title'&rowlimit=5&sortlist='Created:descending'",
                        headers: { "Accept": "application/json; odata=verbose" },
                        contentType: "application/json; odata=verbose",
                        success: function (data, textStatus) {                    
                            var results;                            
                            if (data.d) {
                                if (data.d.query)
                                    results = data.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results;
                                else if (data.d.postquery)
                                    results = data.d.postquery.PrimaryQueryResult.RelevantResults.Table.Rows.results;
                                else
                                    throw "Results not found";
                                if (results.length === 0) {
                                    SPObject.PostMessage(source, origin, '""', '');
                                    return;
                                }
                                else {
                                    SPObject.PostMessage(source, origin, TransformSearchResults(results), '');
                                }
                            }
                        },
                        error: function (data, errorCode, errorMessage) {
                            if (data.error.message !== undefined) {
                                SPObject.PostMessage(source, origin, '""', data.error.message.value);
                            }
                            else {
                                SPObject.PostMessage(source, origin, '""', data.responseText);
                            }
                        }
                    });
                },
                PostMessage : function PostMessage(source, origin, data, error) {
                    source.postMessage('{"Message":'+data+', "Error":"'+error+'"}', origin)               
                }
            };
        })();

        SP.SOD.executeOrDelayUntilScriptLoaded(
            function () { SPObject = new SilverIT.SPIntegrationDemo(); }, "SP.js");        

        window.onmessage = function (e) {
            if (event.origin == 'http://connectionserver) {
                var o = JSON.parse(event.data);
                if (CurrentUser == null) {
                    SPObject.PostMessage(
                        event.source, event.origin, '', 'Cannot be used within an anonymous context')
                }
                switch (o.OperationType) {
                    case 1:                        
                            SPObject.GetMyRecentDocuments(event.source, event.origin);                                                                                         
                            break;
                    case 2:
                        SPObject.GetMyRecentPresentations(event.source, event.origin);
                        break;
                    default:
                        throw "Unknown Operation";
                }
            }            
        };
    })();
</script>.

Here is more or less the sequence of the operations:

  • The code declares an object that during its instantiation gets the identity of the current user. Alternatively, you can use _spPageContextInfo if you have it present in your page
  • The code loads SP.js and instantiates the SPIntegrationDemo object
  • The page registers for the onmessage event
  • When a message is received, it checks the origin, parses the data, determines the operation type and call the relevant method of the SPIntegrationDemo object accordingly.
  • The two methods perform a query against the SharePoint search engine using the REST API
  • The search results are parsed by a common private method that transforms them into a simpler JSON string
  • The data is returned to the sender via postMessage

Authentication

One of the advantages of using an IFRAME is of course the authentication since the browser will consider the remote domain just as a normal domain and
will handle the authentication as it does usually.

Security concerns

This technique can be a wide open door for cross-site scripting attacks. Therefore, you need to make sure you always check the origin of the sender. When registering for the onmessage handler, the browser will be able to receive messages from everywhere including malicious pages. To secure it even better, you can parse the data and check that what you receive is well what you expect to receive. This precaution is necessary since you might not have control over the sender which can itself be attacked by malicious applications and thus, somehow forwarding the attack to you.

Happy Coding!

Advertisements

About Stephane Eyskens

Office 365, Azure PaaS and SharePoint platform expert
This entry was posted in html5, Javascript and tagged , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s