Monday, November 10, 2014

How to copy files between sites using JavaScript REST in Office365 / SharePoint 2013

I’m currently playing with a POC for an App, and wanted to try to do the App as a SharePoint hosted one, only using JavaScript and REST.

The starting point was to call _vti_bin/ExcelRest.asmx on the host web from my app web, but this end-point does neither support CORS nor JSONP, so it can’t be used directly. My next thought was; Ok, let’s copy the file from the host web over to my app web, then call ExcelRest locally. Easier said than done!

While the final solution seems easy enough, the research, trial and error have taken me about 3 days. I’m now sharing this with you so you can spend your valuable time increasing the international GDP instead.

Note: If you want to copy files between two libraries on the same level, then you can use the copyTo method. http://server/site/_api/web/folders/GetByUrl('/site/srclib')/Files/getbyurl('madcow.xlsx')/copyTo(strNewUrl = '/site/targetlib/madcow.xlsx,bOverWrite = true)

Problem

Copy a file from a document library in one site to a document library in a different site using JavaScript and REST.
The code samples have URL’s using the App web proxy, but it’s easily modifiable for non-app work as well.

Step 1 – Reading the file

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var hostweburl = decodeURIComponent(getParameterByName('SPHostUrl'));
var appweburl = decodeURIComponent(getParameterByName('SPAppWebUrl'));
 
var fileContentUrl = "_api/SP.AppContextSite(@target)/web/GetFileByServerRelativeUrl('/site/library/madcow.xlsx')/$value?@target='" + hostweburl + "'";
 
var executor = new SP.RequestExecutor(appweburl);
var info = {
    url: fileContentUrl,
    method: "GET",
    binaryStringResponseBody: true,
    success: function (data) {
        //binary data available in data.body
        var result = data.body;
    },
    error: function (err) {
        alert(JSON.stringify(err));
    }
};
executor.executeAsync(info);

The important parameter here is setting binaryStringResponseBody to true. Without this parameter the response is being decoded as UTF-8 and the result in the success callback is garbled data, which leads to a corrupt file on save.

The  binaryStringResponseBody parameter is not documented anywhere, but I stumbled upon binaryStringRequestbody in an msdn article which was used when uploading a file, and I figured it was worth a shot. Opening SP.RequestExecutor.debug.js I indeed found this parameter.

Step 2 – Patching SP.RequestExecutor.debug.js

Adding binaryStringResponseBody will upon return of the call cause a script error as seen in the figure below.

image


The method in question is reading over the response byte-by-byte from an Uint8Array, building a correctly encoded string. The issue is that it tries to concatenate to a variable named ret, which is not defined. The defined variable is named $v_0, and here we have a real bug in the script. The bug is there both in Office365 and SharePoint 2013 on-premise.

Luckily for us patching JavaScript is super easy. You merely override the methods involved somewhere in your own code before it’s being called. In the below sample it’s being called once the SP.RequestExecutor.js library has been loaded. The method named BinaryDecode is the one with the error, but you have to override more methods as the originator called is internalProcessXMLHttpRequestOnreadystatechange, and it cascades to calling other internal functions which can be renamed at random as the method names are autogenerated. (This happened for me today and I had to change just overrinding the first function).

?
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
$.getScript(scriptbase + "SP.RequestExecutor.js", function(){
SP.RequestExecutorInternalSharedUtility.BinaryDecode = function SP_RequestExecutorInternalSharedUtility$BinaryDecode(data) {
   var ret = '';
 
   if (data) {
      var byteArray = new Uint8Array(data);
 
      for (var i = 0; i < data.byteLength; i++) {
         ret = ret + String.fromCharCode(byteArray[i]);
      }
   }
   ;
   return ret;
};
 
SP.RequestExecutorUtility.IsDefined = function SP_RequestExecutorUtility$$1(data) {
   var nullValue = null;
 
   return data === nullValue || typeof data === 'undefined' || !data.length;
};
 
SP.RequestExecutor.ParseHeaders = function SP_RequestExecutor$ParseHeaders(headers) {
   if (SP.RequestExecutorUtility.IsDefined(headers)) {
      return null;
   }
   var result = {};
   var reSplit = new RegExp('\r?\n');
   var headerArray = headers.split(reSplit);
 
   for (var i = 0; i < headerArray.length; i++) {
      var currentHeader = headerArray[i];
 
      if (!SP.RequestExecutorUtility.IsDefined(currentHeader)) {
         var splitPos = currentHeader.indexOf(':');
 
         if (splitPos > 0) {
            var key = currentHeader.substr(0, splitPos);
            var value = currentHeader.substr(splitPos + 1);
 
            key = SP.RequestExecutorNative.trim(key);
            value = SP.RequestExecutorNative.trim(value);
            result[key.toUpperCase()] = value;
         }
      }
   }
   return result;
};
 
SP.RequestExecutor.internalProcessXMLHttpRequestOnreadystatechange = function SP_RequestExecutor$internalProcessXMLHttpRequestOnreadystatechange(xhr, requestInfo, timeoutId) {
   if (xhr.readyState === 4) {
      if (timeoutId) {
         window.clearTimeout(timeoutId);
      }
      xhr.onreadystatechange = SP.RequestExecutorNative.emptyCallback;
      var responseInfo = new SP.ResponseInfo();
 
      responseInfo.state = requestInfo.state;
      responseInfo.responseAvailable = true;
      if (requestInfo.binaryStringResponseBody) {
         responseInfo.body = SP.RequestExecutorInternalSharedUtility.BinaryDecode(xhr.response);
      }
      else {
         responseInfo.body = xhr.responseText;
      }
      responseInfo.statusCode = xhr.status;
      responseInfo.statusText = xhr.statusText;
      responseInfo.contentType = xhr.getResponseHeader('content-type');
      responseInfo.allResponseHeaders = xhr.getAllResponseHeaders();
      responseInfo.headers = SP.RequestExecutor.ParseHeaders(responseInfo.allResponseHeaders);
      if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 1223) {
         if (requestInfo.success) {
            requestInfo.success(responseInfo);
         }
      }
      else {
         var error = SP.RequestExecutorErrors.httpError;
         var statusText = xhr.statusText;
 
         if (requestInfo.error) {
            requestInfo.error(responseInfo, error, statusText);
         }
      }
   }
};
}); 

Step 3 – Uploading the file

The next step is to save the file in a library on my app web. The crucial part again is to make sure the data is being treated as binary, this time withbinaryStringRequestBody set to true. Make a note of the digest variable as well. On a page inheriting the SP masterpage you can get this value with $("#__REQUESTDIGEST").val(). If not then you have to execute a separate call to _api/contextinfo. The code for that is at the bottom of this post.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var appweburl = decodeURIComponent(getParameterByName('SPAppWebUrl'));
var executor = new SP.RequestExecutor(appweburl);
var info = {
    url: "_api/web/GetFolderByServerRelativeUrl('/appWebtargetFolder')/Files/Add(url='madcow.xlsx', overwrite=true)",
    method: "POST",
    headers: {
        "Accept": "application/json; odata=verbose",
        "X-RequestDigest": digest
    },
    contentType: "application/json;odata=verbose",
    binaryStringRequestBody: true,
    body: data.body,
    success: function(data) {
         alert("Success! Your file was uploaded to SharePoint.");
    },
    error: function (err) {
        alert("Oooooops... it looks like something went wrong uploading your file.");
    }
};
executor.executeAsync(info);

Journey

I started out using jQuery.ajax for my REST calls, but I did not manage to get the encoding right no matter how many posts I read on this. I read through a lot on the following links which led me to the final solution:

Get the digest value

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$.ajax({
    url: "_api/contextinfo",
    type: "POST",
    contentType: "application/x-www-url-encoded",
    dataType: "json",
    headers: {
        "Accept": "application/json; odata=verbose",
    },
    success: function (data) {
        if (data.d) {
            var digest = data.d.GetContextWebInformation.FormDigestValue;
        }
    },
    error: function (err) {
        alert(JSON.stringify(err));
    }
});

Sunday, November 9, 2014

Dynamically Loading Controllers and Views with AngularJS/$controllerProvider and RequireJS

Copyright from: weblogs.asp.net/dwahlin

A complete sample application that uses the techniques shown in this post can be found athttps://github.com/DanWahlin/CustomerManager.
AngularJS provides a simple way to associate a view with a controller and load everything at runtime using the$routeProvider object. Routing code is typically put in a module’s config() function and looks similar to the following:
$routeProvider
     .when('/customers',
        {
            controller: 'CustomersController',
            templateUrl: '/app/views/customers.html'
        })
    .when('/customerorders/:customerID',
        {
            controller: 'CustomerOrdersController',
            templateUrl: '/app/views/customerOrders.html'
        })
    .when('/orders',
        {
            controller: 'OrdersController',
            templateUrl: '/app/views/orders.html'
        })
    .otherwise({ redirectTo: '/customers' });

While this type of code works great for defining routes it requires controller scripts to be loaded upfront in the main shell page by default. That works fine in some scenarios but what if you have a lot of controller scripts and views in a given application and want to dynamically load them on-the-fly at runtime? One way of dealing with that scenario is to define a resolve property on each route and assign it a function that returns a promise. The function can handle dynamically loading the script containing the target controller and resolve the promise once the load is complete. An example of using the resolve property is shown next:

$routeProvider
    .when('/customers',
        {
            templateUrl: '/app/views/customers.html',
            resolve: resolveController('/app/controllers/customersController.js')
        });

This approach works well in cases where you don’t want all of your controller scripts loaded upfront, but it still doesn’t feel quite right – at least to me. I personally don’t like having to define two paths especially if you’ll be working with a lot of routes. If you’ve ever worked with a framework that uses convention over configuration then you’ll know that we can clean up this code by coming up with a standard convention for naming views and controllers. Coming up with a convention can help simplify routes and maintenance of the application over time. The approach that I’ll demonstrate in this post uses the following routing code to define the path, view and controller: 

$routeProvider
    .when('/customers', route.resolve('Customers'))
    .when('/customerorders/:customerID', route.resolve('CustomerOrders'))
    .when('/orders', route.resolve('Orders'))
    .otherwise({ redirectTo: '/customers' });


Notice that a single value is passed into the route.resolve() function. Behind the scenes the function will automatically create the path to the view and the path to the controller based on some simple conventions and then load the appropriate files dynamically. You can access a sample (work in progress) project athttps://github.com/DanWahlin/CustomerManager. Let’s take a look at how it works. 

Dynamically Loading Controllers

The following diagram shows the different players involved in simplifying routes and dynamically loading controllers. RequireJS is used to dynamically load controller JavaScript files and make an application’s main module available to the controllers so that they’re registered properly after they’re loaded. 
image

Here’s how it works:
  1. A file named main.js defines custom scripts that will be loaded using RequireJS. I originally defined 3rd party libraries such as AngularJS in main.js as well but decided there simply wasn’t enough benefit over loading them at the bottom of the initial page using a

Tuesday, November 4, 2014

Impersonation in ASP.NET causes [COMException (0x80072020): An operations error occurred. ]

Copyright from: sharepoint-tweaking.blogspot.com

When you run code that uses DirectorySearcher, DirectoryEntry or other classes that communicates with network resources from a webpart in a Sharepoint site, you recieve a: [COMException (0x80072020): An operations error occurred. ]
This is caused by the fact that when a user is authenticated against a sharepoint server using NTLM or Kerberos, a "secondary token" is sent to the server that it uses to authenticate the user. This token cannot be used to authenticate the current user against another server (e.g. a domain controller).
This can be circumvented by reverting the impersonation to the application pool account used by IIS (if this account has access to Active Directory) with the following code (this is equal to running with impersonation set to false in web.config):

using System.Web.Hosting;
...
...
// Code here runs as the logged on user
using (HostingEnvironment.Impersonate()) {// This code runs as the application pool user
     DirectorySearcher searcher ...
}

// Code here runs as logged on user again