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.
- // update these values to match your site and credentials
- var usr = 'username@yoursite.onmicrosoft.com';
- var pwd = 'password';
- var siteFullUrl = "https://yoursite.sharepoint.com";
- var linkListName = "SharedLinks";
- 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 loginUrl = siteFullUrl + "/_forms/default.aspx?wa=wsignin1.0";
- var authReq = '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">'
- authReq += ' <s:Header>'
- authReq += ' <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>'
- authReq += ' <a:ReplyTo>'
- authReq += ' <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>'
- authReq += ' </a:ReplyTo>'
- authReq += ' <a:To s:mustUnderstand="1">https://login.microsoftonline.com/extSTS.srf</a:To>'
- authReq += ' <o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">'
- authReq += ' <o:UsernameToken>'
- authReq += ' <o:Username>' + usr + '</o:Username>'
- authReq += ' <o:Password>' + pwd + '</o:Password>'
- authReq += ' </o:UsernameToken>'
- authReq += ' </o:Security>'
- authReq += ' </s:Header>'
- authReq += ' <s:Body>'
- 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">'
- authReq += ' <a:EndpointReference>'
- authReq += ' <a:Address>' + loginUrl + '</a:Address>'
- authReq += ' </a:EndpointReference>'
- authReq += ' </wsp:AppliesTo>'
- authReq += ' <t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>'
- authReq += ' <t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>'
- authReq += ' <t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>'
- authReq += ' </t:RequestSecurityToken>'
- authReq += ' </s:Body>'
- authReq += '</s:Envelope>';
- function querySPLinks() {
- // clear former results before submitting the new query
- var resultsDiv = document.getElementById("results");
- while (resultsDiv.childNodes.length > 0) {
- var child = resultsDiv.childNodes[0];
- resultsDiv.removeChild(child);
- }
- getToken();
- }
- // Step 1: we get the token from the STS
- var token;
- function getToken() {
- WinJS.xhr({
- url: "https://login.microsoftonline.com/extSTS.srf",
- type: 'POST',
- data: authReq,
- headers: { 'Content-type': 'application/soap+xml; charset=utf-8' }
- }).done(
- function fulfilled(result) {
- // extract the token from the response data
- token = result.responseXML.querySelector("BinarySecurityToken").textContent;
- getFedAuthCookies();
- },
- function errHandler(err) {
- var e = err;
- });
- }
- // Step 2: "login" using the token provided by STS in step 1
- function getFedAuthCookies() {
- WinJS.xhr({
- url: loginUrl,
- type: 'POST',
- data: token,
- headers: { 'Content-type': 'application/x-www-form-urlencoded' }
- }).done(
- function fulfilled(result) {
- refreshDigest();
- },
- function errHandler(err) {
- var e = err;
- });
- }
- // Step 3: get the digest from the Sites web service and refresh the one stored locally
- var digest;
- function refreshDigest() {
- WinJS.xhr({
- url: siteFullUrl + '/_vti_bin/sites.asmx',
- type: 'POST',
- headers: {
- 'SOAPAction': 'http://schemas.microsoft.com/sharepoint/soap/GetUpdatedFormDigestInformation',
- 'X-RequestForceAuthentication': 'true',
- 'Content-type': 'text/xml; charset=utf-8'
- },
- data: tokenReq
- }).done(
- function fulfilled(result) {
- digest = result.responseXML.querySelector("DigestValue").textContent;
- sendRESTReq();
- },
- function errHandler(err) {
- resportError(err);
- });
- }
- // Step 4: execute the REST request
- function sendRESTReq() {
- WinJS.xhr({
- url: siteFullUrl + "/_api/web/lists/GetByTitle('"+ linkListName + "')/items?$select=URL",
- type: 'GET',
- headers: {
- 'X-RequestDigest': digest,
- "Accept": "application/json; odata=verbose",
- 'Content-type': 'application/json;odata=verbose'
- }
- }).done(
- function fulfilled(result) {
- var response = JSON.parse(result.responseText);
- var links = response.d.results;
- var resultsDiv = document.getElementById("results");
- for (var i = 0; i < links.length; i++) {
- var link = links[i];
- var desc = link.URL.Description;
- var url = link.URL.Url;
- // we make the comparision on the client side,
- // and build the UI for the links dynamically
- if (desc.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
- var aLink = document.createElement("a");
- var br = document.createElement("br");
- aLink.id = "link" + i;
- aLink.innerText = desc;
- aLink.href = url;
- resultsDiv.appendChild(aLink);
- resultsDiv.appendChild(br);
- }
- }
- },
- function errHandler(err) {
- var e = JSON.parse(err.responseText);
- reportErrorMsg("Error: " + e.error.code + "\n" + e.error.message.value);
- });
- }
- function reportError(msg) {
- if (e.message != undefined) {
- reportErrorMsg(e.message);
- }
- else if (e.statusText != undefined) {
- reportErrorMsg(e.statusText);
- }
- else {
- reportErrorMsg("Error");
- }
- }
- function reportErrorMsg(msg) {
- WinJS.log(msg, "sample", "error");
- }
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
_api/web/lists/GetByTitle(‘SharedLinks’)/items
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.
The results will be displayed in the Output section of the app. Users can open the links in IE 10 simply by a click.
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.
