In this post, I’m going to create a better CORS Wrapper for SharePoint REST operations, and demonstrate using it for CRUD operations on a Picture library. First, I want to remove the dependency of jQuery, using fetch instead. As I covered in a previous post, Ugly SPA Revisited using Fetch and REST, fetch is new enough and implementations are spotty enough, even in evergreen browsers, that I will need to polyfill fetch and ES6 promise in order to support a reasonable cross-section of browsers.
By implementing the full range of CRUD operations on document libraries, we’ll have an opportunity to see if there are other issues that need to be addressed in our CORS Wrapper. My last post really only did one simple REST operation across CORS boundaries.
I’m going to build the same ugly SPA I’ve built before in some of my REST posts. In particular, I’m going to rewrite the code from the post Ugly SPA Revisited using Fetch and REST, this time using my CORS Wrapper to perform CRUD operations across Host-named Site Collections (HNSC). I’ll repeat the basics of what the SPA is supposed to look like in the next section.
The goal of the CORS Wrapper is to make calling web services across CORS boundaries as easy as making web service calls that are not cross-origin. And ideally, the syntax of these cross-origin web service calls should look as close as possible to non-cross-origin web service calls. So by comparing my source code from this post to the source code from Ugly SPA Revisited using Fetch and REST, we’ll be able to see just how successful we were.
What We’re Going to Build
The CORS Wrapper HTML
There is nothing special about the HTML, but since I’m going to be manipulating it as I demonstrate the CRUD operations, I’ll show it here so you can refer back to it. It consists of an unordered list to show pictures currently in the library, a small form for edit/delete, and a div with some structure to implement drag and drop files.
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 | <!--List of pictures (name and radio button)--> <ul id="picturelist" style="list-style: none"> </ul> <!--Update Form--> <div id="updatePanel" class="updatepanel"> Title: <input type='text' id='title' val='' /> Description: <input type='text' id='description' val='' /> <button type='button' id='Update'> Update </button> <button type='button' id='Delete'> Delete </button> </p> <!--Drag and Drop div--> <div id="dragandrophandler"> <div id="draganddropbusy" style="display:none"> <!--Change this path for your environment--> <img src="/_layouts/images/progress.gif" alt="CORS Wrapper Upload Progress..."> </p> <div id="draganddroplabel">Drag & Drop Images To Add</p> </p> |
As I process the read operation and shove list items into the unordered list, I’ll also add some hidden data- attributes to the item to store things like the title, description, id, and etag, that will be needed for other CRUD operations later on, so a list item will end up looking like this:
1 2 3 | <li data-id="333" data-title="Poipu - Hawaii" data-description="Snorkeling" data-etag="4"> <input name="item" id="item333" value="333" type="radio"/>Poipu_Hawaii.jpg </li> |
The CORS Wrapper Service Proxy Implementation
The service proxy implementation is shown below. It’s virtually the same as the service proxy from the previous post, except the dependencies it imports and the URL patterns it allows.
As before, I need to have a WebPartPages:AllowFraming control on the page in order to allow the page to be loaded in a cross-resource iframe.
And of course, instead of loading jQuery as a dependency, now I’m loading a fetch polyfill as a dependency, and also loading an ES6 Promise polyfill if needed.
The two URL patterns I’ve specified below are all that is required in order to allow CRUD operations specifically on the Pictures library.
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 | <%@ Assembly Name="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Page Language="C#" Inherits="Microsoft.SharePoint.WebPartPages.WikiEditPage" MasterPageFile="~masterurl/default.master" %> <%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Import Namespace="Microsoft.SharePoint" %> <asp:Content ContentPlaceHolderID='PlaceHolderPageTitle' runat='server'> Service Proxy Test Page </asp:Content> <asp:Content ContentPlaceHolderID='PlaceHolderPageTitleInTitleArea' runat='server'> </asp:Content> <asp:Content ContentPlaceHolderID='PlaceHolderAdditionalPageHead' runat='server'> <WebPartPages:AllowFraming runat="server"></WebPartPages:AllowFraming> <script type="text/javascript"> window.Promise || document.write('<script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.js"><\/script>'); </script> <script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@2.0.4/fetch.js"></script> </asp:Content> <asp:Content ContentPlaceHolderID='PlaceHolderMiniConsole' runat='server'> </asp:Content> <asp:Content ContentPlaceHolderID='PlaceHolderLeftActions' runat='server'> </asp:Content> <asp:content contentplaceholderid="PlaceHolderMain" runat="server"> <h2>ServiceProxy Test Page</h2> <script type="text/javascript" src="/Style Library/spcors.js"></script> <input name="btnTest" id="btnTest" type="button" value="Test Local"> <script type="text/javascript"> (function() { // the server relative url of the web var base = (_spPageContextInfo.webServerRelativeUrl === "/" ? "" : _spPageContextInfo.webServerRelativeUrl); // a list of patterns to match against the origin var originPatterns = [ new RegExp("^https://dev\.wingtip\.com$") ]; // a list of patterns to match against the ajax url var urlPatterns = [ new RegExp( "^https://speasyforms\.wingtip\.com/_api/web/lists/getbytitle.'Pictures'.", "i"), new RegExp( "^https://speasyforms\.wingtip\.com/_api/web/getfilebyserverrelativeurl.'" + base + "/Pictures/", "i") ]; // Create a new instance of ServiceProxy. var sp = new SPCORS.ServiceProxy("*", originPatterns, urlPatterns); })(); </script> </asp:content> |
The CORS Wrapper Proxy Client Implementation
This implementation is going to be quite large for a blog post, so I’m not going to show the full source code here. I’ll just show the script for various operations and include the full source as a download at the end of the post. And in general, I’m not going to talk much about the code, but rather just point out differences between this code and the code of Ugly SPA Revisited using Fetch and REST, since that’s really what we’re interested in for this post. For more information on the basic code, consult that post.
CORS Read
Below is the implementation of the read operation in the ugly SPA, using the CORS wrapper. I’ve highlighted just the lines of code that have changed (as compared to Ugly SPA Revisited using Fetch and REST). And it isn’t that many lines, which is great since the goal was to make CORS as easy as plain old Ajax. The changes are:
- First, of course, I need to instantiate an instance of SPCORS.ProxyClient, and lock it down by specifying origin patterns for site collections that are allowed to message us.
- Then, replace the call to fetch with a call to spcors.fetch.
And that’s it. The arguments that are passed to fetch don’t change, and the result is exactly the same. This seems like a good start.
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 | // a list of patterns to match against the origin var originPatterns = [ new RegExp("^https://speasyforms\.wingtip\.com$") ]; // instantiate the cors proxy client var spcors = new SPCORS.ProxyClient("spproxy", "*", originPatterns); var listTitle = "Pictures"; var uploading = 0; /*========================================================= Read in images from the picture library. =========================================================*/ function readImages() { var index = 0; var selected = document.querySelector("input[name='item']:checked"); if (selected) { index = selected.value; } var pictureList = document.getElementById("picturelist"); var serviceUrl = "/_api/Web/Lists/getByTitle('" + listTitle + "')/Items"; var serviceParams = "$select=FileRef,Title,Description,Id,Created,Modified,GUID&$expand=File"; var url = "~site" + serviceUrl + "?" + serviceParams; spcors.fetch(url, { method: "GET", headers: { 'accept': 'application/json;odata=nometadata' } }).then(function(json) { for (var i = 0; i < json.value.length; i++) { var current = json.value[i]; current.Name = current.FileRef; current.Name = current.Name.substr(current.Name.lastIndexOf("/") + 1); current.Title = current.Title || ""; current.Description = current.Description || ""; if (current.File && current.File.ETag) { var tmp = current.File.ETag; if (tmp.indexOf(",") > 0) { current.eTag = tmp.substr(tmp.indexOf(",") + 1); } } var input = document.getElementById("item" + current.ID); if (input) { var li = input.parentNode; li.setAttribute("data-title", current.Title); li.setAttribute("data-description", current.Description); if (current.eTag) li.setAttribute("data-etag", current.etag); } else { var container = document.createElement("div"); container.innerHTML = "<li data-id='" + current.ID + "' " + "data-title='" + current.Title + "' " + "data-description='" + current.Description + "' " + (current.eTag ? "data-etag=''" + current.eTag + "''" : "") + ">" + "<input type='radio' name='item' id='item" + current.ID + "' value='" + current.ID + "' />" + current.Name + "</li>"; pictureList.appendChild(container.children[0]); } } if (index) { var current = document.getElementById("item" + index); current.checked = true; var title = current.parentNode.getAttribute("data-title"); var description = current.parentNode.getAttribute("data-description"); document.getElementById("title").value = title; document.getElementById("description").value = description; document.getElementById("updatePanel").style.display = "block"; } else { document.getElementById("updatePanel").style.display = "none"; document.getElementById("title").value = ""; document.getElementById("description").value = ""; } }).catch(function(error) { alertError(error); }); } |
CORS Create
And below is the implementation of create, using the CORS wrapper, again with the changes highlighted. All two lines of them. The changes are:
- Again, change fetch to spcors.fetch.
- Changed the call to reader.readAsArrayBuffer(file); to
reader.readAsDataURL(file);.
Now the first change is obvious enough, but the second change may require a bit of explanation. FileReader.readAsArrayBuffer reads a file as binary data suitable for sending to SharePoint RESTful web services as an image to be saved. The problem is that now we’re using post messages to work around CORS issues. The only way to send post messages between frames in a way that is cross-browser compatible is to send them as a string. So I’m taking the init object that is to be passed to fetch and calling JSON.stringify on it to convert it to a string. And if that init object has a body that is a binary payload, then JSON.stringify is going to mangle that all to hell. Sorry, that’s the actual technical jargon, “mangle that all to hell.”
So I need to encode that binary payload somehow. Encode really just means convert to a JavaScript formatted string. And that’s exactly what readAsDataURL does. It returns a string, in the format of a data URI, with the payload base64 encoded (it may theoretically not be encoded at all or encoded some other way, but for images, it will be base64 encoded). So for instance:
data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D
is a base64 encoded representation of the plain text “Hello%2C%20World!”. The parts of this are:
- data: (indicates a data URI)
- text/plain (the mime type of the payload)
- base64 (the encoding of the payload)
- And a comma, followed by the encoded payload.
Now don’t get all glazed over, you don’t really need to understand this, readAsDataURL is going to take care of all of this for you, so the string that is returned as the file contents will be suitable for transmission as a post message. You should be aware that this encoding is going to bloat the size of the upload by 33%. And while the specification for post messages doesn’t limit the size of post messages, some browsers may.
And I do need to know about data URIs. Because before that data is transmitted to SharePoint, I need to convert it back to binary data. That means that first, I need to recognize it as a data URI, and then I need to decode it, or what gets saved to SharePoint won’t look like an image anymore. I do that work in the ServiceProxy.fetch method, which we’ll get to later.
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 | /*========================================================= Upload an image to a picture library. =========================================================*/ function createImage(file) { var reader = new FileReader(); reader.onloadend = function(evt) { var buffer = evt.target.result; if (uploading == 0) { document.getElementById("draganddroplabel").style.display = "none"; document.getElementById("draganddropbusy").style.display = "block"; } uploading++; function decrement() { uploading--; if (!uploading) { document.getElementById("draganddroplabel").style.display = "block"; document.getElementById("draganddropbusy").style.display = "none"; readImages(); } } var url = "~site" + "/_api/web/lists/getByTitle('" + listTitle + "')" + "/RootFolder/Files/add(url='" + file.name + "',overwrite='true')?" + "@TargetLibrary='" + listTitle + "'&@TargetFileName='" + file.name + "'"; UpdateFormDigest(_spPageContextInfo.webServerRelativeUrl, _spFormDigestRefreshInterval); spcors.fetch(url, { method: "POST", headers: { "accept": "application/json;odata=nometadata", "X-RequestDigest": document.getElementById("__REQUESTDIGEST").value }, body: buffer }).then(function(json) { decrement(); }).catch(function(error) { alertError(error); }); }; reader.readAsDataURL(file); } |
CORS Update and Delete
And that’s really the last interesting thing I have to say about UglySpaClient.aspx. The update and delete operations are identical to those from Ugly SPA Revisited using Fetch and REST, except that all calls to fetch have been changed to spcors.fetch. I’ve included the code in the download, but there isn’t much point in showing it here, and there’s more than enough code in this post already!
The CORS Wrapper Implementation
I’m going to briefly talk about some of the more interesting aspects of the two main classes, ServiceProxy and ProxyClient, below, and otherwise just dump the code on you.
The ServiceProxy Class
The most interesting thing about the ServiceProxy class is that it converts data URIs in the body back to binary content. The method dataUri2Blob does the work of unencoding data uris before sending the body to the RESTful web service if needed. This is a bit involved. I’ll include the references I used to figure out how to do it at the end.
Other than that, I just converted the $.ajax calls to fetch. And again, since this class depends on ES6 Promises and a pretty robust implementation of fetch, to get broad cross-browser support you’ll need to polyfill them.
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 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | /** * Represents a proxy that listens for postMessage events and attempts to * convert them to SharePoint ajax requests using fetch, send the response back as a * postMessage to the parent window (assumes running in an iframe). * @constructor * @param (string) origin - The url expected for the response origin, will * accept any origin if set to "*". * @param [string] originPatterns - message handler will post an exception * message if this array is defined and contains at least one RegExp and the * origin does not match any of these patterns. * @param [string] urlPatterns - ajax will post an exception message if this * array is defined and contains at least one RegExp and the * ajax url does not match any of these patterns. * @returns {object} a new ServiceProxy instance. */ function ServiceProxy(origin, originPatterns, urlPatterns) { var _self = this; /** * Call fetch and send a post message with either the result or an error * message. * @param {object} init - the input to fetch. Note that fetch takes the url as a * separate parameter, but I do not, because post message can only easily * send a single object, so I split out the url before calling fetch. * @returns undefined - the result is sent back to the parent window as a post * message using the respond method. */ this.fetchWrapper = function(init) { init = init || {}; var url = init.url; // split out the url try { // transform substitution tokens url = url.replace(/~site/g, _spPageContextInfo.webAbsoluteUrl); url = url.replace(/~sitecollection/g, _spPageContextInfo.siteAbsoluteUrl); // throw exception if urlPatterns test fails gaurde(url, urlPatterns, "URL"); // always include credentials init.credentials = "include"; // turn arbitrary objects into Headers if (init && init.headers && !(init.headers instanceof Headers)) { var newHeaders = new Headers(); for (var key in init.headers) { newHeaders.append(key, init.headers[key]); } init.headers = newHeaders; } // update the form digest if necessary UpdateFormDigest(_spPageContextInfo.webServerRelativeUrl, _spFormDigestRefreshInterval); // get the request digest, this has to be gotten in the service proxy // since the digest is site collection specific and we're cross-origin init.headers.set("X-RequestDigest", document.getElementById("__REQUESTDIGEST").value); if (init.body && init.body.match(/^data:[a-z]*\/[a-z]*;base64,/i)) { this.dataUri2Blob(init); } } catch (e) { // send the error as a post message _self.respond(init, "exception", e.message + "\n\n" + e.stack); } // Call fetch just as you would the global fetch, but headers // can be an arbitrary JSON object. fetch(url, init) .then(function(response) { // non-success response if (response.status < 200 || response.status >= 400) { throw new Error(response.status + " " + response.statusText); } // no content by design if (response.status === 204) { return response.text(); } // no content, but not not by design? if (response.headers.get("content-length") === "0") { return response.text(); } // process json responses if (init.headers) { var accept = init.headers.get("accept"); if (accept && accept.indexOf("application/json") > -1) { var contentType = response.headers.get("content-type"); if (contentType && contentType.indexOf("application/json") > -1) { return response.json(); } else { throw new TypeError("Oops, should have gotten JSON, but didn't!"); } } } // no error, but also not JSON return response.text(); }).then(function(data) { // send the response as a post message _self.respond(init, "ok", data); }).catch(function(error) { // send the error as a post message _self.respond(init, "exception", error.message + "\n\n" + error.stack); }); return; } /** * Convert a data url body to binary. * @init {object} - the fetch init object. This method assumes the body is a * data uri, so check first. * @returns undefined - the body of the init object is updated with the binary * contents if successful. */ this.dataUri2Blob = function(init) { // get the base64 encoded file contents from the data url var bytes = atob(init.body.split(',')[1]); // get the mime type from the data url var mimeType = init.body.split(',')[0].split(':')[1].split(';')[0]; // create an array buffer and decode the bytes to it var buffer = new ArrayBuffer(bytes.length); var uia = new Uint8Array(buffer); for (var i = 0; i < bytes.length; i++) { uia[i] = bytes.charCodeAt(i); } try { // create a data view and instantiate a new Blob by mime type var dataView = new DataView(buffer); init.body = new Blob([dataView], { type: mimeType }); } catch (e) { // Old browser, need to use blob builder, including all versions of // internet explorer window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder; if (window.BlobBuilder) { // if there is a blob builder, append the array buffer to it // and get the contents by mime type. var blobBuilder = new BlobBuilder(); blobBuilder.append(buffer); init.body = blobBuilder.getBlob(mimeType); } } return; }, /** * Send an fetch response (or exception) back to the calling frame via * post message. * @param {options} - The options passed to fetch. * @param (string) - A status message included in the response, generally * either 'ok' or 'exception'. * @param {data} the data returned from the web service, or a string * exception message. * @return (undefined) will call respond to send the response as a * post message. */ this.respond = function(options, status, data) { var response = { options: options, status: status, message: data }; parent.postMessage(JSON.stringify(response), origin); return; }; /** * Create a listener for postMessage events and pass the data to fetch */ window.addEventListener("message", function(e) { if (typeof(e.data) === "string") { var message; try { // deserialize the json data message = JSON.parse(e.data); } catch (e) { /* assume message for someone else */ } // if not json, then not a message for this handler if (message) { // throw an exception if originPatterns do not match origin gaurde(e.origin, originPatterns, "Origin"); // pass the message to this.ajax _self.fetchWrapper(message); } } }, false); } // attach class definition to namespace SPCORS.ServiceProxy = ServiceProxy; |
The ProxyClient Class
And last but not least, below is the ProxyClient implementation. It has no dependency on fetch, but it does depend on ES6 Promises so it won’t work on older browsers (including all versions of Internet Explorer) without a polyfill.
Converting the ProxyClient class to use fetch and ES6 promises were not intuitive to me. This is because of some fundamental differences between jQuery Deferreds and ES6 Promises.
I can instantiate a jQuery Deferred instance without any arguments, and call resolve or reject on that instantiation later to keep my promise.
But with an ES6 Promise, you have to pass it a function on instantiation. And the promise doesn’t have resolve and reject methods, resolve and reject callbacks are passed into the function that you give to the Promise constructor. It assumes that inside that function, you’re going to do something asynchronous, like Ajax, and then call resolve or reject when your asynchronous operation resolves.
But our asynchronous operation isn’t going to happen inside this function, it’s going to happen in a post message event receiver. Previously, I saved the Deferred so I could resolve the promise. Now, I have to save the resolve and reject callbacks too, because they’re not members of the promise. Not a big change, but it’s a change, and it confused me at first. What can I say, sometimes I’m easily confused.
This implementation has one other improvement over my jQuery-based implementation. All of the work is done inside the callback to the promise constructor. It’s all wrapped inside a try/catch block. And if it catches an exception, it rejects the promise. The previous implementation created a deferred and did some other processing, and then returned the deferred. If that other processing threw an exception, then it was thrown from the function, rather than returned a promise that gets rejected. This violates a simple rule; promise-based functions should not throw exceptions. The alternative is that callers would need to handle exceptions in two different ways.
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 | /** * Represents a consumer of SharePoint ajax requests using postMessage to * execute requests safely in another domain (i.e. a consumer of postMessages * from ServiceProxy). * @constructor * @param (string) iframeid - the id of the iframe DOM element that implements * the ServiceProxy. * @param (string) origin - The url expected for the response origin, will * accept any origin if set to "*". * @param [string] originPatterns - message handler will post an exception * message if this array is defined and contains at least one RegExp and the * origin does not match any of these patterns. * @returns {object} a new ProxyClient instance. */ function ProxyClient(iframeid, origin, originPatterns) { var _self = this; // Private map of promises by guid (the guid // is generated when the request is made). When a post message is // received, the message handler looks for the guid, retrieves the // promise, resolves/rejects the promise, and finally deletes the promise from the map // so it can only be called once and the map doesn't grow indefinitely. var promises = {}; /** * Use this method just as you would use fetch. * @param {string} url - the url to use for the ajax request. * @param {object} init - the initialization object for fetch. See fetch * documentation for details. * @returns {object} - a promise which will be resolved or recjected based * on the outcome of the fetch operation. */ this.fetch = function(url, init) { // create a new promise var promise = new Promise(function(resolve, reject) { try { // get a handle to the iframe var iframe = document.getElementById(iframeid).contentWindow; // add the url to the init object to be passed over // to the service proxy. init.url = url; // create a new guid for the request init.guid = SP.Guid.newGuid().toString(); // add the promise by guid to the promises map, along // with its resolve and reject callbacks, which will get // call back later by the post message handler promises[init.guid] = { promise: promise, resolve: resolve, reject: reject }; // post the options as a message to the proxy frame iframe.postMessage(JSON.stringify(init), origin); } catch(e) { reject(e); } }); // return the promise return promise; }; /* * Crate a listener for postMessages and pass the data to this.ajax. */ window.addEventListener("message", function(e) { var promise; try { // we expect a string, if not the message may be for a // different listener if (typeof(e.data) !== "string") return; // deserialize the message var data = e.data.trim(); try { data = JSON.parse(data); } catch (e) {} // we expect an object, with an options property that is also // an object, if not the message may be for a different listener if (typeof(data) !== "object" || typeof(data.options) !== "object") return; // if we have a promise for this message for this message if (data.options.guid && promises[data.options.guid]) { // get the promise promise = promises[data.options.guid]; if (window !== window.top) { { // if the origin doesn't match expectations, throw an exception gaurde(e.origin, originPatterns, "Origin"); } if (data.status === "exception") { // exception, reject the promise promise.reject(data.message); } else { // resolve the promise promise.resolve(data.message); } } } catch (e) { // reject the promise if we got far enough to find one if (typeof(promise) !== "undefined") promise.reject(e); } }, false); } // attach class definition to namespace SPCORS.ProxyClient = ProxyClient; |
Sum Up
This concludes my series on CORS and SharePoint. Hopefully, I’ve demonstrated that with some work CORS can be just as easy to work with as Ajax. And before you turn your nose up at using my CORS Wrapper, remember that Ajax wasn’t all that fun when all we had was XMLHttpUtility to work with. If you do any work with HNSCs, you’re likely to run up against the CORS wall at some point, and post message is nasty to work with on a project of any significant complexity without some sort of abstraction. There may be some edge cases where this needs to be adjusted to work with specific web service calls, but at this point, I can’t imagine what those could be?
References
- dataURItoBlob – David Gomez-Urquiza
- BlobBuilder – brettjthom
- Promise-based functions should not throw exceptions – Dr. Axel Rauschmayer
FetchCORSWrapper.zip – the complete source for this post.