This post will show you how to opt-in to enabling Client App Parts from Apps for SharePoint to post Status Bar and Notification area alerts to the hosting page.
Overview
Client App Parts are essentially iframes that can be placed on SharePoint pages. Because Apps for SharePoint run in a different domain name than the host web, Client App Parts are by default restricted from accessing anything on the host page. This makes them pretty “dumb” islands on the page, as they can’t be aware of anything on the host page, and can’t interact with host page elements through the DOM.
This post will show how you can use the postMessage api to enable your Apps for SharePoint to write Status Bar and toaster Notification alerts to the hosting page.
Creating the SharePoint App
For this demonstration, I’m going to use a simple SharePoint-hosted app, but the following will also work with a Provided-hosted app part as well. I’ll start by creating a new SharePoint-hosted app, called StatusBarApp, and then add a Client Web Part to the project, called StatusBarAppPart, with a new page called StatusBarAppPart.aspx:
I’ll open up StatusBarAppPart.aspx and add a reference to my App.js file:
1 2 3 4 5 6 7 8 |
" ]<head> <title></title> <script type="text/javascript" src="../Scripts/jquery-1.9.1.min.js"></script> <script type="text/javascript" src="/_layouts/15/MicrosoftAjax.js"></script> <script type="text/javascript" src="/_layouts/15/sp.runtime.js"></script> <script type="text/javascript" src="/_layouts/15/sp.js"></script> <script type="text/javascript" src="../Scripts/App.js"></script> |
I’ll add the following markup to the page in the <body>
:
1 2 3 4 5 6 7 8 9 10 11 12 |
<body> <div> <p id="message"> <button id="buttonStatus">Click to send status to host page.</button> </p> <p> <button id="buttonNotification">Click to send a toaster notification to host page.</button> </p> </div> </body> |
Basically it’s two buttons that I can use to add status and notification alerts to the hosting page.
After that, I’ll switch to the App.js file, and replace the contents with the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
'use strict'; function getQueryStringParameter(key, urlToParse) { /// <signature> /// <summary>Gets a querystring parameter, case sensitive.</summary> /// <param name="key" type="String">The querystring key (case sensitive).</param> /// <param name="urlToParse" type="String">A url to parse.</param> /// </signature> /// <signature> /// <summary>Gets a querystring parameter from the document's URL, case sensitive.</summary> /// <param name="key" type="String">The querystring key (case sensitive).</param> /// </signature> if (!urlToParse || urlToParse.length === 0) { urlToParse = document.URL; } if (urlToParse.indexOf("?") === -1) { return ""; } var params = urlToParse.split('?')[1].split('&'); for (var i = 0; i < params.length; i = i + 1) { var singleParam = params[i].split('='); if (singleParam[0] === key) { return decodeURIComponent(singleParam[1]); } } return ""; } var messageToPost = { message: "", senderId: getQueryStringParameter("SenderId") }; $(document).ready(function () { // Setup click events on buttons. $("#buttonStatus").click(sendStatusMessage); $("#buttonNotification").click(sendNotification); }); function sendStatusMessage() { messageToPost.message = "SP.UI.Status.AddStatus"; messageToPost.title = "Status:"; messageToPost.html = "<span>Some <em>italicized</em> text posted from App Part!.</span>"; messageToPost.color = "blue"; messageToPost.atBegining = true; window.parent.postMessage(JSON.stringify(messageToPost), document.referrer); return false; } function sendNotification() { messageToPost.message = "SP.UI.Notify.AddNotification"; messageToPost.title = "<span>Notification with <em>italicized text</em> from App Part!"; messageToPost.isSticky = false; window.parent.postMessage(JSON.stringify(messageToPost), document.referrer); return false; } |
I’ll deploy the app, and place the Client App Part on a host-web web part page:
Right now, the buttons will send postMessages, but nothing will happen. You’ll notice that I am sending the message over as a stringified version of a JSON object. This can then be rehydrated on the hosting page as a valid JSON object. In the next section, I’ll add the receiving code and wire everything up.
Adding the Opt-In Code to the Host Page
Using postMessage is a two-sided handshake. One side can send messages, but if the recipient isn’t listening for them, the messages are completely ignored. In order to have the host page receive the messages from the App Part, some script needs to be placed on the host page to listen and respond accordingly. This script I am going to place in a ScriptEditor web part. The following snippet will make the host page listen for the messages, and create status and notification alerts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
<script type="text/javascript"> (function() { var statusListener = function (e) { console.log("Handler called."); var messageData; try { messageData = JSON.parse(e.data); } catch (error) { console.log("Could not parse the message response."); return; } if (!messageData) { console.log("Message response was empty."); return; } // Validate that it contains the expected instruction if (messageData.message !== "SP.UI.Status.AddStatus" && messageData.message !== "SP.UI.Notify.AddNotification") { console.log("Invalid message."); return; } if (messageData.message === "SP.UI.Status.AddStatus") { var sid = SP.UI.Status.addStatus(messageData.title, messageData.html, messageData.atBegining); SP.UI.Status.setStatusPriColor(sid, messageData.color); console.log("Added status."); } else { var nid = SP.UI.Notify.addNotification(messageData.title, messageData.isSticky); console.log("Notification added."); } }; // Register the listener if (typeof window.addEventListener !== 'undefined') { window.addEventListener('message', statusListener , false); } else if (typeof window.attachEvent !== 'undefined') { window.attachEvent('onmessage', statusListener ); } })(); </script> |
With this code in place, I can now click the buttons, and see new status messages and toaster notifications on the host page:
Cool, right?
To make this even better, you can implement the operations to remove status, update/append status, and clear notifications, using postMessage actions from the host page to the App Part iframe, sending back the status or notification ids (the reverse direction of what was configured above).
Securing Messages
When you use postMessage to send cross-domain messages between iframes and windows, you have the potential of letting malicious code get sent and executed. To prevent this, you’ll need to harden the messaging system and throw away invalid messages. To see an example of this hardening, let’s take a look at how SharePoint itself manages the security for its built-in resize postMessage handler. Following you can see the code that gets added to a host page whenever it contains at least one Client App Part on the page:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
" ]<script type='text/javascript'> var spAppIFrameSenderInfo = new Array(1); var SPAppIFramePostMsgHandler = function(e) { if (e.data.length > 100) return; var regex = RegExp(/(<\s*[Mm]essage\s+[Ss]ender[Ii]d\s*=\s*([\dAaBbCcDdEdFf]{8})(\d{1,3})\s*>[Rr]esize\s*\(\s*(\s*(\d*)\s*([^,\)\s\d]*)\s*,\s*(\d*)\s*([^,\)\s\d]*))?\s*\)\s*<\/\s*[Mm]essage\s*>)/); var results = regex.exec(e.data); if (results == null) return; var senderIndex = results[3]; if (senderIndex >= spAppIFrameSenderInfo.length) return; var senderId = results[2] + senderIndex; var iframeId = unescape(spAppIFrameSenderInfo[senderIndex][1]); var senderOrigin = unescape(spAppIFrameSenderInfo[senderIndex][2]); if (senderId != spAppIFrameSenderInfo[senderIndex][0] || senderOrigin != e.origin) return; var width = results[5]; var height = results[7]; if (width == "") { width = '300px'; } else { var widthUnit = results[6]; if (widthUnit == "") widthUnit = 'px'; width = width + widthUnit; } if (height == "") { height = '150px'; } else { var heightUnit = results[8]; if (heightUnit == "") heightUnit = 'px'; height = height + heightUnit; } var widthCssText = ""; var resizeWidth = ('False' == spAppIFrameSenderInfo[senderIndex][3]); if (resizeWidth) { widthCssText = 'width:' + width + ' !important;'; } var cssText = widthCssText; var resizeHeight = ('False' == spAppIFrameSenderInfo[senderIndex][4]); if (resizeHeight) { cssText += 'height:' + height + ' !important'; } if (cssText != "") { var webPartInnermostDivId = spAppIFrameSenderInfo[senderIndex][5]; if (webPartInnermostDivId != "") { var webPartDivId = 'WebPart' + webPartInnermostDivId; var webPartDiv = document.getElementById(webPartDivId); if (null != webPartDiv) { webPartDiv.style.cssText = cssText; } cssText = ""; if (resizeWidth) { var webPartChromeTitle = document.getElementById(webPartDivId + '_ChromeTitle'); if (null != webPartChromeTitle) { webPartChromeTitle.style.cssText = widthCssText; } cssText = 'width:100% !important;' } if (resizeHeight) { cssText += 'height:100% !important'; } var webPartInnermostDiv = document.getElementById(webPartInnermostDivId); if (null != webPartInnermostDiv) { webPartInnermostDiv.style.cssText = cssText; } } var iframe = document.getElementById(iframeId); if (null != iframe) { iframe.style.cssText = cssText; } } } if (typeof window.addEventListener != 'undefined') { window.addEventListener('message', SPAppIFramePostMsgHandler, false); } else if (typeof window.attachEvent != 'undefined') { window.attachEvent('onmessage', SPAppIFramePostMsgHandler); } spAppIFrameSenderInfo[0] = new Array("04885D9A0","g_a32cdeb7_500b_4d41_96bd_99b968b8fdaa","https:\u002f\u002fdomainnameremoved-b84e6d0e047207.sharepoint.com","True","False","ctl00_ctl34_g_06e70b34_2f05_400a_a8c7_e6a17ec59506"); </script> |
Notice the first line and last line. The first line declares an array (spAppIFrameSenderInfo) with a specific size. This array size will vary depending on how many Client App Parts are on the hosting page. For each Client App Part, there will be a corresponding item in this array.
1 |
var spAppIFrameSenderInfo = new Array(1); |
Now notice the last line. A line like this is injected on the page for each Client App Part placed on the page, adding specific information about that App Part into the array declared earlier.
1 |
spAppIFrameSenderInfo[0] = new Array("04885D9A0","g_a32cdeb7_500b_4d41_96bd_99b968b8fdaa","https:\u002f\u002fdomainnameremoved-b84e6d0e047207.sharepoint.com","True","False","ctl00_ctl34_g_06e70b34_2f05_400a_a8c7_e6a17ec59506"); |
In the array item representing the App Part, you’ll notice an odd value for the first parameter of the array item. This is a unique alphanumeric number that is dynamically generated fresh on every page request. This value is passed to the Client App Part via the SenderId querystring parameter (if you use the StandardTokens). This SenderId has a couple of parts to it – the first 8 characters are an alphanumeric string randomly generated. Any characters after that represent the index inside of the spAppIFrameSenderInfo array.
Now let’s start looking at the hoops Microsoft has gone through just to ensure that a resize message is secure. First, you’ll notice that Microsoft is using an XML string to pass information across postMessage. Why not JSON here? I can’t speak for Microsoft, but my assumption is that they chose this route because they were worried about malicious code in a JSON object getting rehydrated and executed on the other side. The JSON.parse function in modern browsers is designed to ensure safe execution, and takes a number of steps to make sure that malicious javascript isn’t just blindly executed with an eval() statement. My take: it’s good to be extra cautious, but if you are running a controlled environment with your own SharePoint apps (and tightly controlling where and what 3rd party apps are installed), passing JSON is much easier to deal with than parsing XML.
Next, Microsoft checks to make sure that the entire message isn’t longer than 100 characters:
1 2 |
if (e.data.length > 100) return; |
Next, Microsoft parses the entire message using RegEx, and a host of capture phrases to extract all of the relevant data it needs:
1 2 3 4 |
var regex = RegExp(/(<\s*[Mm]essage\s+[Ss]ender[Ii]d\s*=\s*([\dAaBbCcDdEdFf]{8})(\d{1,3})\s*>[Rr]esize\s*\(\s*(\s*(\d*)\s*([^,\)\s\d]*)\s*,\s*(\d*)\s*([^,\)\s\d]*))?\s*\)\s*<\/\s*[Mm]essage\s*>)/); var results = regex.exec(e.data); if (results == null) return; |
One of the things it captures is the SenderId, which is required to be passed back by the App Part. The SenderId is split into its two parts, and then the second part checked to ensure that it isn’t outside the bounds of the spAppIFrameSenderInfo array:
1 2 3 |
var senderIndex = results[3]; if (senderIndex >= spAppIFrameSenderInfo.length) return; |
Next, the code checks to make sure that the SenderId passed matches the SenderId in the spAppIframeSenderInfo array for the item at the specified index. This ensures that the resize message fires for the correct App Part that sent the message, and only that App Part:
1 2 3 4 5 |
var senderId = results[2] + senderIndex; var iframeId = unescape(spAppIFrameSenderInfo[senderIndex][1]); var senderOrigin = unescape(spAppIFrameSenderInfo[senderIndex][2]); if (senderId != spAppIFrameSenderInfo[senderIndex][0] || senderOrigin != e.origin) return; |
You can see that the validation here is quite intense, just to get a resize message across. Using similar validation approaches, I can now harden the earlier code in the ScriptEditor web part, to be more secure:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
" ]if (!messageData) { console.log("Message response was empty."); return; } if (!messageData.senderId || messageData.senderId === 'undefined') { return; } var regex = RegExp(/([\dAaBbCcDdEdFf]{8})(\d{1,3})/); var results = regex.exec(messageData.senderId); if (results == null) { return; } var senderIndex = results[2]; if (senderIndex >= spAppIFrameSenderInfo.length) { return; } var senderId = results[1] + senderIndex; var senderOrigin = unescape(spAppIFrameSenderInfo[senderIndex][2]); if (senderId != spAppIFrameSenderInfo[senderIndex][0] || senderOrigin != e.origin) { return; } // Validate that it contains the expected instruction if (messageData.message !== "SP.UI.Status.AddStatus" && messageData.message !== "SP.UI.Notify.AddNotification") { console.log("Invalid message."); return; } |
Summary
Hopefully I’ve shown how you can use postMessage api to enable Client App Parts to interact with the host page and post Status and Notification items. You can further expand this concept to other things, like popping up Modal Dialogs, displaying Wait Dialogs or loading indicators, or other interactive parts of the SharePoint host page.
Do you know what creates the settings for the Array
spAppIFrameSenderInfo[0] = new Array("04885D9A0","g_a32cdeb7_500b_4d41_96bd_99b968b8fdaa","https:\u002f\u002fdomainnameremoved-b84e6d0e047207.sharepoint.com","True","False","ctl00_ctl34_g_06e70b34_2f05_400a_a8c7_e6a17ec59506");
I am specifically looking for the
spAppIFrameSenderInfo[senderIndex][3]
and the
spAppIFrameSenderInfo[senderIndex][4]
values. In my source, they are both set to True. When the when this line executes
var resizeWidth = ('False' == spAppIFrameSenderInfo[senderIndex][3]);
it doesn’t re-size the frame.
Am I missing something here?
Writing my last comment a minute ago sparked something in my brain I guess.
The problem was that I set width and height to be fixed inside the edit part settings.
I changed the height and width back to the “Fit to zone” option and my postMessage Script worked.
Thank you for this article, it was well written and very helpful.