In the recent weeks I got an idea of a specific client-side HTML application (HTML file located on the local hard drive) that communicate with the SharePoint server. More on that idea later, as I hopefully achieve some results, but for now I would like to share a few interesting byproducts of my research.
This time I start with a few questions: what can we do, if we need a client application that manipulates SharePoint objects on a computer that doesn’t have (or can’t have) the managed client object model installed on it? Let’s say, you don’t have even the necessary .NET runtime version, or you don’t have Visual Studio, you have no former C# / PowerShell experience (or at least, you would not like to bother with them as you need a quickly editable application), so assume all you know is HTML and the good old JavaScript. You can’t deploy files to the SharePoint site, you don’t have even page edit permissions, so the standard way of injecting JavaScript through the Content Editor Web Part (CEWP) is not available to you. Using the REST API from your HTML pages through web requests could be an option, but it might be simply not powerful enough (especially in SharePoint 2010 version) to achieve your goals.
Using the ECMAScript Client Object Model (aka JavaScript Client Object Model or JSCOM) would be ideal to this task, but it has a significant out-of-the-box limitation: it was designed to work in the SharePoint site context it interacts with, that means, you can use only relative URLs when instantiating the SP.ClientContext object, or get the current context from the SharePoint page the script is used on, none of these is possible when working with a local HTML file.
In the last years I already delved quite deep into the client object model (both the managed and the JavaScript versions), and learned that even the out-of-the-box asynchronous calls can be altered to synchronous, so we should never give up.
In this post I show you a possible workaround for the problem in the case of an on-premise SharePoint 2010 that authenticates users through standard windows-integrated authentication. In the next post I plan to illustrate an even more complex solution for the case of the Office 365 Preview (that is a cloud-based SharePoint 2013).
As always, Fiddler proved again to be an invaluable tool analyzing the network traffic caused by the managed OM and trying to simulate the same for ECMAScript.
NOTE: As in the case of the former hacks, be aware that the methods described here don’t use the public, documented interfaces, so it is probably not supported by MS, and suggested to be used only as an experiment. There is no guarantee that it will work for you, especially if our environments are at different SP / cumulative update level.
In the progress of my work, I faced two major (and several minor) issues:
1. How to enforce using of absolute site URLs in JSCOM instead of the relative ones? It was the easier problem to solve as you see it soon.
2. How to achieve the right request digest token to get authorization to access the server side object from the JavaScript code.
Prerequisites to start the work with JSCOM in the local HTML files:
- References to the SharePoint JavaScript libraries (and jQuery if you would like to work with that). These ones are included “automatically” when working with standard SharePoint pages, but in this case we should reference them (in the correct order!) as well. We could start our custom scripts only after our page and these libraries are already loaded.
- A defined JavaScript variable (see more about that below) called _spPageContextInfo. That is in our case only a dummy placeholder (can be copied from the source of a SharePoint page) that the scripts depend on.
- A HTML input field (by default it is a hidden one) called __REQUESTDIGEST that contains the authentication digest for the page. It can be copied from the source of a SharePoint page as well, but it has an expiration time. It is no problem in the case of a web page, as it is refreshed on the server site on page reloads, but it is not the case in the local file, so we should get always an actual one (see problem 2 above).
Regarding Issue 1: After a short investigation I found that the URL the client requests are sent to is determined by the $1P_0 property of the SP.ClientContext object. We should set our custom value right after initializing the context.
var siteRelativeUrl = "/";
var siteFullUrl = "http://intranet.contoso.com";
var context = new SP.ClientContext(siteRelativeUrl); // we set a dummy relative path that always exists
context.$1P_0 = siteFullUrl;
Setting the absolute URL is not enough for the successful request. Next step was to solve the issue of the request digest as without that our request would be rejected. It took me a while, but at the end it turned to be the same token as the one returned by the GetUpdatedFormDigestInformation method of the Sites web service. So all we have to do is to call this method with the properly formatted request, and replace the value of the __REQUESTDIGEST field before calling the actual JSCOM code.
On of the minor issues was, how to send POST requests to another domain (issue with cross-domain scripting), in this case to access the Sites web service. For GET requests there is JSONP (you can set crossDomain: true for your AJAX request in jQuery), but for POST, it is not available, crossDomain had simply no effect during my tests. In this situation I received an “Invalid argument.” error thrown in MicrosoftAjax.js when setting headers for the web requests.
Fortunately I found this jQuery option that solved this problem for me:
$.support.cors = true;
NOTE: When you open your HTML page not from the file system, but rather through another web server, you might be faced with the following prompt:
Choosing the default value (that is “No”) results in an “Access is denied.” error in MicrosoftAjax.js.
NOTE: For the sake of simplicity, I’ve uploaded a jQuery version (v1.8.2 to be exact, as the version number may be important) to my SharePoint server. In the real life one can reference for example http://code.jquery.com/jquery-latest.min.js instead of this.
Finally, here is the full source code of a “minimized” page that displays the number and the name of the lists on a remote SharePoint site. It is assumed that you are online (access to SP) and are (or can be) authenticated by the SharePoint server, having minimum read permissions on the site:
- <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/jquery/jquery.min.js"></script>
- <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/1033/init.js"></script>
- <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/MicrosoftAjax.js"></script>
- <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/CUI.js"></script>
- <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/1033/Core.js"></script>
- <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/SP.Core.js"></script>
- <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/SP.Runtime.js"></script>
- <script type="text/ecmascript" src="http://intranet.contoso.com/_layouts/SP.js"></script>
- <input type="hidden" name="__REQUESTDIGEST" id="__REQUESTDIGEST" value="0x0B5280FBC219C9A7E41746D9EA6AC24E65CB936560A6856AA4C38997F114401AED3254D7DDFACBE03C2028F689D0E67F55239E8FA679CE15F3EBC7A0C6280A34,05 Jan 2013 21:34:32 -0000" />
- <script language="ecmascript" type="text/ecmascript">
- // define token request XML
- var tokenReq = '<?xml version="1.0" encoding="utf-8"?>';
- tokenReq += '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">';
- tokenReq += ' <soap:Body>';
- tokenReq += ' <GetUpdatedFormDigestInformation xmlns="http://schemas.microsoft.com/sharepoint/soap/" />';
- tokenReq += ' </soap:Body>';
- tokenReq += '</soap:Envelope>';
- var siteFullUrl = "http://intranet.contoso.com";
- var lists;
- function startScript() {
- refreshDigest();
- }
- function refreshDigest() {
- $.support.cors = true; // enable cross-domain query
- $.ajax({
- type: 'POST',
- data: tokenReq,
- crossDomain: true, // had no effect, see support.cors above
- contentType: 'text/xml; charset="utf-8"',
- url: siteFullUrl + '/_vti_bin/sites.asmx',
- headers: {
- 'SOAPAction': 'http://schemas.microsoft.com/sharepoint/soap/GetUpdatedFormDigestInformation',
- 'X-RequestForceAuthentication': 'true'
- },
- dataType: 'xml',
- complete: function (result) {
- $('#__REQUESTDIGEST').val($(result.responseXML).find("DigestValue").text());
- sendJSCOMReq();
- }
- });
- }
- function sendJSCOMReq()
- {
- var _spPageContextInfo = {webServerRelativeUrl: "\u002f", webLanguage: 1033, currentLanguage: 1033, webUIVersion:4,pageListId:"{7bbd4c55-f832-40e2-8e2a-243455c3b2ba}",pageItemId:1,userId:1073741823, alertsEnabled:true, siteServerRelativeUrl: "\u002f", allowSilverlightPrompt:'True'};
- var siteRelativeUrl = "/";
- var context = new SP.ClientContext(siteRelativeUrl); // we set a dummy relative path that always exists
- context.$1P_0 = siteFullUrl;
- var web = context.get_web();
- lists = web.get_lists();
- context.load(lists);
- context.executeQueryAsync(Function.createDelegate(this, this.onQuerySucceeded), Function.createDelegate(this, this.onQueryFailed));
- }
- function onQuerySucceeded(sender, args) {
- var count = lists.get_count();
- var listTitles = "Number of lists: " + count + ":\n";
- for(var i=0;i<count; i++)
- {
- var list = lists.get_item(i);
- listTitles += " " + list.get_title() + "\n";
- }
- alert(listTitles);
- }
- function onQueryFailed(sender, args) {
- alert("Request failed: "+ args.get_message());
- }
- // start the custom script execution after the scripts and page are loaded
- SP.SOD.executeOrDelayUntilScriptLoaded(function () {
- $(document).ready(startScript);
- }, "sp.js");
- </script>
After solving these problems I had a bad feeling. What happens if I authenticate myself as a standard user with low privileges, then stole the digest of a site owner included in a SharePoint page using a network traffic analyzer tool and try to send my request with the token of the other user? My experiments show that this issue is handled by SharePoint, as I received an error stating the token was not valid and I had to get a new one from the server.
In the next post I take another step forward to show you how to achieve the same using the Office 365 Preview version.