Second Life of a Hungarian SharePoint Geek

February 24, 2013

Favorites in the Cloud: Implementing a Windows 8 Search application with An Office 365 backend

In the previous part of this post I illustrated, how we can share links from IE 10 on O365. For general background info on the subject I suggest you to read that part first. In this part I provide the code for an app that helps to look up the favorites and to open them in IE.

Just like in the former part, I used a sample project from the Windows 8 SDK as a prototype of the development, and concentrate on the SharePoint-specific code only. In this case we use the Search contract sample\JavaScript solution as the framework of the development.

The solution

After opening the Search contract sample solution in Visual Studio 2012, we have to make a few minor modifications in the code, and a major one to achieve the goals.

First, open the sample-utils.js file, look up the ScenarioOutput control, and append the following code snippet at the end of its declaration, after _addStatusOutput (don’t forget the comma separator!):

,
_addResultsOutput: function (element) {
    var resultsDiv = document.createElement("div");
    resultsDiv.id = "results";
    element.insertBefore(resultsDiv, element.childNodes[0]);
}

This div serves as the content placeholder when displaying the results.

In the default.js file we should first find this code block:

// Scenario 1 : Support receiving a search query while running as the main application.
Windows.ApplicationModel.Search.SearchPane.getForCurrentView().onquerysubmitted = function (eventObject) {
    WinJS.log && WinJS.log("User submitted the search query: " + eventObject.queryText, "sample", "status");
};

and replace it with this one (changes are highlighted with yellow):

var query;
// Scenario 1 : Support receiving a search query while running as the main application.
Windows.ApplicationModel.Search.SearchPane.getForCurrentView().onquerysubmitted = function (eventObject) {
    WinJS.log && WinJS.log("User submitted the search query: " + eventObject.queryText, "sample", "status");
    query = eventObject.queryText;
    querySPLinks();

};

Next, add this larger code snippet at the end (but before the closing braces!) of the default.js, and update the credential and the site URL. In a real-world app you should of course prompt for the credentials and optionally store them in a secure location.

Code Snippet
  1. // update these values to match your site and credentials
  2. var usr = 'username@yoursite.onmicrosoft.com';
  3. var pwd = 'password';
  4. var siteFullUrl = "https://yoursite.sharepoint.com";
  5. var linkListName = "SharedLinks";
  6.  
  7. var tokenReq = '<?xml version="1.0" encoding="utf-8"?>';
  8. tokenReq += '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance&quot; xmlns:xsd="http://www.w3.org/2001/XMLSchema&quot; xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">&#039;;
  9. tokenReq += '  <soap:Body>';
  10. tokenReq += '    <GetUpdatedFormDigestInformation xmlns="http://schemas.microsoft.com/sharepoint/soap/&quot; />';
  11. tokenReq += '  </soap:Body>';
  12. tokenReq += '</soap:Envelope>';
  13.  
  14. var loginUrl = siteFullUrl + "/_forms/default.aspx?wa=wsignin1.0";
  15. var authReq = '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope&quot; xmlns:a="http://www.w3.org/2005/08/addressing&quot; xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">&#039;
  16. authReq += '  <s:Header>'
  17. authReq += '    <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>&#039;
  18. authReq += '    <a:ReplyTo>'
  19. authReq += '      <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>&#039;
  20. authReq += '    </a:ReplyTo>'
  21. authReq += '    <a:To s:mustUnderstand="1">https://login.microsoftonline.com/extSTS.srf</a:To>&#039;
  22. authReq += '    <o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">&#039;
  23. authReq += '      <o:UsernameToken>'
  24. authReq += '        <o:Username>' + usr + '</o:Username>'
  25. authReq += '        <o:Password>' + pwd + '</o:Password>'
  26. authReq += '      </o:UsernameToken>'
  27. authReq += '    </o:Security>'
  28. authReq += '  </s:Header>'
  29. authReq += '  <s:Body>'
  30. authReq += '    <t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust"><wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">&#039;
  31. authReq += '      <a:EndpointReference>'
  32. authReq += '        <a:Address>' + loginUrl + '</a:Address>'
  33. authReq += '      </a:EndpointReference>'
  34. authReq += '      </wsp:AppliesTo>'
  35. authReq += '      <t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>&#039;
  36. authReq += '      <t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>&#039;
  37. authReq += '      <t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>'
  38. authReq += '    </t:RequestSecurityToken>'
  39. authReq += '  </s:Body>'
  40. authReq += '</s:Envelope>';
  41.  
  42. function querySPLinks() {
  43.     // clear former results before submitting the new query
  44.     var resultsDiv = document.getElementById("results");
  45.     while (resultsDiv.childNodes.length > 0) {
  46.         var child = resultsDiv.childNodes[0];
  47.         resultsDiv.removeChild(child);
  48.     }
  49.     getToken();
  50. }
  51.  
  52. // Step 1: we get the token from the STS
  53. var token;
  54. function getToken() {
  55.     WinJS.xhr({
  56.         url: "https://login.microsoftonline.com/extSTS.srf&quot;,
  57.         type: 'POST',
  58.         data: authReq,
  59.         headers: { 'Content-type': 'application/soap+xml; charset=utf-8' }
  60.     }).done(
  61.     function fulfilled(result) {
  62.         // extract the token from the response data
  63.         token = result.responseXML.querySelector("BinarySecurityToken").textContent;
  64.         getFedAuthCookies();
  65.     },
  66.     function errHandler(err) {
  67.         var e = err;
  68.     });
  69. }
  70.  
  71. // Step 2: "login" using the token provided by STS in step 1
  72. function getFedAuthCookies() {
  73.     WinJS.xhr({
  74.         url: loginUrl,
  75.         type: 'POST',
  76.         data: token,
  77.         headers: { 'Content-type': 'application/x-www-form-urlencoded' }
  78.     }).done(
  79.     function fulfilled(result) {
  80.         refreshDigest();
  81.     },
  82.     function errHandler(err) {
  83.         var e = err;
  84.     });
  85. }
  86.  
  87. // Step 3: get the digest from the Sites web service and refresh the one stored locally
  88. var digest;
  89. function refreshDigest() {
  90.     WinJS.xhr({
  91.         url: siteFullUrl + '/_vti_bin/sites.asmx',
  92.         type: 'POST',
  93.         headers: {
  94.             'SOAPAction': 'http://schemas.microsoft.com/sharepoint/soap/GetUpdatedFormDigestInformation&#039;,
  95.             'X-RequestForceAuthentication': 'true',
  96.             'Content-type': 'text/xml; charset=utf-8'
  97.         },
  98.         data: tokenReq
  99.     }).done(
  100.    function fulfilled(result) {
  101.        digest = result.responseXML.querySelector("DigestValue").textContent;
  102.        sendRESTReq();
  103.    },
  104.    function errHandler(err) {
  105.        resportError(err);
  106.    });
  107. }
  108.  
  109. // Step 4: execute the REST request
  110. function sendRESTReq() {
  111.     WinJS.xhr({
  112.         url: siteFullUrl + "/_api/web/lists/GetByTitle('"+ linkListName + "')/items?$select=URL",
  113.         type: 'GET',
  114.         headers: {
  115.             'X-RequestDigest': digest,
  116.             "Accept": "application/json; odata=verbose",
  117.             'Content-type': 'application/json;odata=verbose'
  118.         }
  119.     }).done(
  120.    function fulfilled(result) {
  121.        var response = JSON.parse(result.responseText);
  122.        var links = response.d.results;
  123.        var resultsDiv = document.getElementById("results");
  124.        for (var i = 0; i < links.length; i++) {
  125.            var link = links[i];
  126.            var desc = link.URL.Description;
  127.            var url = link.URL.Url;
  128.            // we make the comparision on the client side,
  129.            // and build the UI for the links dynamically
  130.            if (desc.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
  131.                var aLink = document.createElement("a");
  132.                var br = document.createElement("br");
  133.                aLink.id = "link" + i;
  134.                aLink.innerText = desc;
  135.                aLink.href = url;
  136.                resultsDiv.appendChild(aLink);
  137.                resultsDiv.appendChild(br);
  138.            }
  139.        }
  140.    },
  141.    function errHandler(err) {
  142.        var e = JSON.parse(err.responseText);
  143.        reportErrorMsg("Error: " + e.error.code + "\n" + e.error.message.value);
  144.    });
  145. }
  146.  
  147. function reportError(msg) {
  148.     if (e.message != undefined) {
  149.         reportErrorMsg(e.message);
  150.     }
  151.     else if (e.statusText != undefined) {
  152.         reportErrorMsg(e.statusText);
  153.     }
  154.     else {
  155.         reportErrorMsg("Error");
  156.     }
  157. }
  158.  
  159. function reportErrorMsg(msg) {
  160.     WinJS.log(msg, "sample", "error");
  161. }

How does it work?

The authentication mechanism against O365 is the same that I applied in my former post, and we use REST and WinJS.xhr as in the former part of this post.

Instead of selecting the matching items only using CAML, we load all items from the list to the client side, and filter the items in the client app. To limit the bandwidth usage we limit the scope of requested data to the URL property, that includes both the URL and the title of the link.

_api/web/lists/GetByTitle(‘SharedLinks’)/items?$select=URL

I found that if one would like to submit a REST request that includes a field of type URL as a filter, like the query below, a HTTP 400 status is returned by the server. However, when using IE alone to submit the query, we got no info on the reason.

_api/web/lists/GetByTitle(‘SharedLinks’)/items?$select=URL&$filter=startswith(URL,’code’)

First, when monitoring with Fiddler, could we recognize, that an XML document is returned as the body of the response with an error message:

The field ‘URL’ of type ‘URL’ cannot be used in the query filter expression.

Sad, but true.

BTW, using the following two images I illustrate, how the format of the fields of type URL has been changed in the response from the SP2010-style REST (_vti_bin/ListData.svc) to the SP2013-style (_vti_bin/client.svc or simply _api).

_vti_bin/ListData.svc/SharedLinks

image

_api/web/lists/GetByTitle(‘SharedLinks’)/items

image

Testing the search app

After deploying the app, in Window 8 you can activate the Search charm using the Windows + Q shortcut. You should select the Search contract JS sample from the available list, then provide the query term, for example, “project”, and submit the query.

image

The results will be displayed in the Output section of the app. Users can open the links in IE 10 simply by a click.

image

This app and the former one illustrate, that it is rather easy to integrate the cloud based SharePoint with your custom Windows 8 apps using the new, enhanced REST API. Having this type of extensions one can already organize the favorites in IE.

Advertisements

Leave a Comment »

No comments yet.

RSS feed for comments on this post. TrackBack URI

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

Blog at WordPress.com.

%d bloggers like this: