I don’t know if it is just me, but a considerable part of my “knowledge” on SharePoint (and on other topics as well) is stored as links to interesting articles and nice blog posts. Whenever I need to refresh my memory on a specific topic, I know which texts I should read to get into the context as quick as possible. My friends receive typically a batch of links to such must-reads when they ask my help on a theme.
Organizing the links in a way that enables finding the right ones easily is an art and science of its own. The built-in tools for organizing and finding links (e.g. favorites) in Internet Explorer are rather limited, for example, you can store a link only in a single location (folder), no tagging, no rating, etc. A custom IE add-in can definitely make one’s life easier and the work more effective there.
My pain – The (very) limited favorites in IE 10 (Metro)
When you work with the touch-optimized (a.k.a Metro) version of IE in Windows 8, you have even less out-of-the-box options to handle your links.
You can pin the current page to he favorites.
When you activate the URL text box, the favorites are displayed as tiles. The built-in favorites UI seems to be not optimized for hundreds of links. There is no way to organize the links into folders or search them, at least I found no option to manage, but to remove the selected one.
You are not allowed to install add-ins in this version of IE, so how to tweak these limitations?
The solution
The simplest way I found to extend the default features is implementing a Share Target application to store your links in a backend system, and a Search application to look up the links. This backend system could be the file system, a database, or even O365.
Overview of the idea
In Windows 8 IE acts as a Share Source application. We should create two Windows 8 apps. First app is a Share Target that uses the Windows 8 Share charm to enable users to store the actual visited page as a link in SharePoint online. The second app lets the users to search the saved links using the Search charm by participating in the Search contract.
In this post I show you a proof-of-concept of a JavaScript application that acts as a Share Target for links and stores them in a Links list on a O365 site, the Search app will be the theme of a next post. As general in the case of POC apps, I concentrate on the main issue, that is interacting with O365 from a JS W8 app, and other – also important – issues are ignored for the sake of simplicity. For example, we store user name and password hardcoded in the app. In a real-world app it is a “worst practice”, you should prompt the user for the credentials and optionally store them in a secure location. Error handling in this app is also very lightly implemented.
As an introduction to the theme of developing Share Target applications you can read this article on MSDN.
If you don’t have it yet, you should download the Windows 8 SDK sample applications from here. I use one of the sample apps (Sharing content target app sample\JavaScript) as the boilerplate of the development of my Share Target app.
But before launching Visual Studio, I prepared the storage place for my links. On my O365 Developer Site I created a new Links list,
and named it SharedLinks:
That’s all about preparation, let’s start Visual Studio 2012 on W8, and open the JS version of the Sharing content target app sample solution!
In target.html, look up the code for button reportCompleted:
<button id="reportCompleted">Report Completed</button>
and insert this snippet before that text:
<div>
<button id="shareWithO365">Share on O365</button>
</div>
<br />
In target.js, first extend the inititalize function with this line of code to register the event handler method for our new button:
document.getElementById("shareWithO365").addEventListener("click", shareWithO365, false);
then append the following code at the end of the file (but before the closing braces!), and update the credential and the site URL:
- 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>';
- // 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 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 shareWithO365() {
- // start a long-running share operation
- reportStarted();
- 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) {
- reportErrorEx(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) {
- reportErrorEx(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) {
- reportErrorEx(err);
- });
- }
- // Step 4: execute the REST request
- function sendRESTReq() {
- var title = document.getElementById("title").innerText;
- var url = document.getElementById("description").innerText;
- WinJS.xhr({
- url: siteFullUrl + "/_api/web/lists/GetByTitle('" + linkListName + "')/items",
- type: 'POST',
- headers: {
- 'X-RequestDigest': digest,
- "Accept": "application/json; odata=verbose",
- 'Content-type': 'application/json;odata=verbose'
- },
- data: '{"__metadata":{"type":"SP.Data.' + linkListName + 'ListItem"},"URL": {"Description": "' + title + '", "Url": "' + url + '"}}'
- }).done(
- function fulfilled(result) {
- // long-running share operation completed
- reportCompleted();
- },
- function errHandler(err) {
- var respText = JSON.parse(err.responseText);
- reportErrorMsg("Error: " + respText.error.code + "\n" + respText.error.message.value);
- });
- }
- function reportErrorEx(e) {
- if (e.message != undefined) {
- reportErrorMsg(e.message);
- }
- else if (e.statusText != undefined) {
- reportErrorMsg(e.statusText);
- }
- else {
- reportErrorMsg("Error");
- }
- }
- function reportErrorMsg(msg) {
- document.getElementById("extendedShareErrorMessage").value = msg;
- // long-running share operation failed
- reportError();
- }
How does it work?
The authentication mechanism against O365 is pretty the same that I applied in my former post, however I chose REST instead of the ECMAScript OM this time (see reasons below), and instead of jQuery and its ajax method we should go with WinJS.xhr in the W8 app.
You can read more about REST in SP 2013 here, and WinJS.xhr is documented here.
Note: When working with the SharePoint REST API, you should officially get the digest token through the contextinfo operator (short described here), as illustrated by the code bellow. However, I found no difference between this approach, and using the Sites web service as earlier.
- function getRESTDigest() {
- WinJS.xhr({
- url: siteFullUrl + '/_api/contextinfo', // or '/lists/SharedLinks/_api/contextinfo'
- type: 'POST'
- }).done(
- function fulfilled(result) {
- digest = result.responseXML.querySelector("FormDigestValue").textContent;
- sendRESTReq();
- },
- function errHandler(err) {
- reportErrorEx(err);
- });
- }
Testing the share app
When the users would like to share a link from IE, they could press Windows + H to open the Share charm, and then click on the Share Target JS sample to activate our sharing app.
When the user clicks on the Share on O365 button, we start a long-running share operation, and authenticates the app against O365, then save the link using the REST API.
If there is no error, we report completed for the share operation (see fulfilled function in sendRESTReq), and you should see the new shared link in the SharedLinks list on O365 short after clicking on the Share on O365 button.
However, if there is an error, we report the failure (see reportErrorEx and reportErrorMsg functions), and you should see a notification popup.
In this case, at the bottom of the Share charm appears a similar warning. By opening it, you can see the exact details of the failure.
As you can see, this time I specified a non-existing list name to emulate an error condition.
Why JavaScript/HTML?
To tell the truth, the main reason is rather selfish: I was to learn so much new things as possible, and after the first experiments I found the C# solution simply less exciting / trendy (although it was far from trivial as well), while JavaScript/HTML promised a lot to discover. The secondary reason was that I hoped a more platform-(or device)-independent result (at least, in the context of W8, WP8, W8RT), however I have to say, that after reading more on the compatibility issues between these devices I am not sure I achieved that goal. But at least, I tried…
Should you find this solution trivial and look for even more challenges, you can read my comments below regarding the ECMAScript OM.
Why not the JavaScript / ECMAScript Object Model?
To tell the truth, it was my first idea to use the ECMAScript OM solution from my former post to implement the Share Target because of the simplicity and the broad API support of the OM, and although it was not trivial, I was able to create an application that works. There are however reasons, not to choose this way if you would not like to get a lot of troubles and support issues.
Main problems
- Windows 8 apps are not allowed to reference external scripts, all script files have to be part of the solution. You can install the SharePoint Foundation 2010 Client Object Model Redistributable or SharePoint Server 2013 Client Components SDK Preview, and find most of the scripts at the SharePoint Client Components\Scripts folder in your Program Files directory. Based on the info in the redist.txt, you are allowed to include this files in your application. However, there are other important .js files (like SP.Core.js) that are not included in the package, but required by the ECMAScript OM runtime. These files can be downloaded from O365 and can be attached to the project, although I found no explicit statement that you are granted to use the files such way.
- ECMAScript OM meant to be used only in the context of a page downloaded from the SharePoint server, and not in the context of an external HTML application. Although we can resolve the technical difficulties involved in the external usage (see my samples for O365 and for on-premise), that is definitely not a supported scenario.
So far so good, we was able to achieve our goal using REST, but what happens, if you need to access resources that are not yet supported by this API (for example, the taxonomy service)? It seems you are out of luck in this case. You can either implement an unsupported solution hacking with the ECMAScript OM (not recommended!), create your wrapper services (and deploy them, for example to Azure; pretty overcomplicated for a simply task in my opinion), or simple forget it / wait for the REST support.
How to retrieve our favorites then?
In this post we saw how to store the links on O365 from our app. In the next part I provide an example of looking-up our favorites and opening them in IE.
