Taking Office Web Apps 2013 and SharePoint 2013 integration one step further

Office Web Apps and SharePoint are integrated in several very cool ways.  For example, you can see the callout menu on a document library to create new documents:

You can view document previews in the callout of a document:

You can see the document preview in the callout of a search result:

One of the things I noticed right off the bat with Office Web Apps 2013 was that the call out menu has the same options for all office content types of a certain type.  This causes some issues when users try to click on the links.

An example of this is the "Follow" link.  If you click on the "Follow" link of a document that is stored in a file share, it will error with the following:

I'm sure you could get into the display template and remove that action, but it wasn't as important as some of the other items on my plate when it comes to OWA and SharePoint Search integration.  For example, you will also notice that you do not get the ability to view a document that lives in the file share in the thumbnail previewer.  I find this unacceptable and so did my customer!  So…I started out on the path to figure out how to get it working!  Here's what I came up with.

The first thing I thought was cool about Office Web Apps is the ability to "embed" documents in your web pages that live pretty much anywhere.  If you open the "http://svr-owa/op/generate.aspx" page, you will see you have the ability to create an embedded link:

Once you create the link to a fileshare document you can put it on any of your HTMLSharePoint pages. However, after creating the link, if you try to open it, you will typically get a "File not found" error:

Turns out this is the main wrapper error around just about anything error that happens in OWA.  The main reason that the files won't display is because of permission issues on the Office Web App server.  You see, it doesn't open the file as you, it opens it as the Office Web Apps service identity!  You can find out what your identity is by opening the IIS Manager on the OWA server and looking for the OpenFromUrlWeb app pool:

By default, this account is set to "Network Service".  This account can't do much with secured network based resources unless you assign it those permissions.  This means doing the whole give DOMAINCOMPUTERNAME$ access to your file share and all the OWA servers in your OWA farm.  This then implies that any software that is running on your OWA servers will then have access to your file share.  If only OWA is installed, then you should be ok, but don't forget about all the other service that are running as NETWORK SERVICE on the services applet.  I'm not a big fan of this and in my case, I made it a specifc OWA domain account that has at least read access to the shares that contain your data. NOTE:  This change is not supported by Microsoft, but let's be clear about what unsupported means.  There are two types of unsupported features, ones they don't have the scripts for at the first level of support, and the ones they do have at second and third levels of support.  In my eyes, this falls into the first category and is a simple change but they have not trained anyone on how to do this or troubleshoot it, so this is at your own discretion.  So why does OWA work like this?  The reason lies in the way that Office Web Apps must access a file
in order to render it.  When it is stored in SharePoint, SharePoint
will pass an OAuth access token that OWA can use to access the file as the
user.  This will always ensure that OWA accesses a file that the user can access.  When accessing through the OpenFromUrl means, it has to access it
as itself using regular windows auth. This has some security implications.  Search does only show files the user has access too, so that isn't really a security hole when using the method below, but where things do get interesting if a savvy user figures out how to construct the web page viewer url to a file they don't have access too.  This can elevate their privileges and allow them to look at a file but not change it.  In this case, you should place a "Deny" ACL on the OWA account (whether domain or network service) to prevent it from reading the document.

If you aren't comfortable in making these changes, then don't. You can take the also not so great approach of simply copying your file share (all 5TB of it) into SharePoint to gain the functionality.

Once that is done, you should now be able to see your documents open in the embedded link generated:

So now that, that works.  How do I get SharePoint Search to open the file as a preview?  Hmmm…tricky.  Let's look at how it does files that live in SharePoint.  In order to do this, we have to open the search display templates for Office documents.  The first one is for Word documents and it is called "Item_Word_HoverPanel.html" (yeah, they all have a different display template so you'll have to repeat the steps for each).  You will notice some JavaScript that looks for a specific property ("ServerRedirectedEmbedURL"):

If this property exists (it is only populated by search if the content lives in SharePoint and is of a specific file type), then it will render the Office Web Apps preview area in the callout.  In order to get OWA to work for fileshare files, we have to add the url that renders the link to the browser view or the embedded iframe.  I wasn't able to get the iframe to work, but I did get a link that users can click on to get another browser to open with the file.  You can do this by modifying the file to have the following:

 <!–#_
        var i = 0;
        var wacurlExist = !Srch.U.e(ctx.CurrentItem.ServerRedirectedURL) && !Srch.U.e(ctx.CurrentItem.ServerRedirectedEmbedURL);
        var id = ctx.CurrentItem.csr_id;
        ctx.CurrentItem.csr_FileType = Srch.Res.file_Word;
        ctx.CurrentItem.csr_ShowFollowLink = true;
        ctx.CurrentItem.csr_ShowViewLibrary = true;
        ctx.currentItem_IsOfficeDocument = true;
        var find = '/';
        var re = new RegExp(find, 'g');
        
        function replaceAll(find, replace, str) {
  return str.replace(new RegExp(find, 'g'), replace);
}
        
_#–>
        <div class="ms-srch-hover-innerContainer ms-srch-hover-wacSize" id="_#= $htmlEncode(id + HP.ids.inner) =#_">
            <div class="ms-srch-hover-arrowBorder" id="_#= $htmlEncode(id + HP.ids.arrowBorder) =#_"></div>
            <div class="ms-srch-hover-arrow" id="_#= $htmlEncode(id + HP.ids.arrow) =#_"></div>
            <div class="ms-srch-hover-content" id="_#= $htmlEncode(id + HP.ids.content) =#_" data-displaytemplate="WordHoverPanel">
                <div id="_#= $htmlEncode(id + HP.ids.header) =#_" class="ms-srch-hover-header">
                    _#= ctx.RenderHeader(ctx) =#_
                </div>
                <div id="_#= $htmlEncode(id + HP.ids.body) =#_" class="ms-srch-hover-body">
                <!–#_
                if ((ctx.CurrentItem.FileType == "docx") && Srch.U.n(ctx.CurrentItem.ServerRedirectedEmbedURL))
                {
                        ctx.CurrentItem.csr_DataShown = true;
                        ctx.currentItem_ShowChangedBySnippet = true;

                _#–>
<a href="https://svr-owa.contosocom/op/view.aspx?src=_#= $urlHtmlEncode(ctx.CurrentItem.Path.replace('file:','').replace(re,'%5C')) =#_" target="_blank">View File</a>

                <!–#_
                }
                _#–>

<!–#_
                    if(!Srch.U.n(ctx.CurrentItem.ServerRedirectedEmbedURL))
                    {
                        ctx.CurrentItem.csr_DataShown = true;
                        ctx.currentItem_ShowChangedBySnippet = true;
_#–>
                        <div class="ms-srch-hover-viewerContainer">
                            <iframe id="_#= $htmlEncode(id + HP.ids.viewer) =#_" src="_#= $urlHtmlEncode(ctx.CurrentItem.ServerRedirectedEmbedURL) =#_" scrolling="no" frameborder="0px" class="ms-srch-hover-viewer"></iframe>
                        </div>
                        <div class="ms-srch-hover-wacImageContainer">
                            <img id="_#= $htmlEncode(id + HP
.ids.preview) =#_" alt="_#= $htmlEncode(Srch.Res.item_Alt_Preview) =#_" onload="this.parentNode.style.display='block';" />
                        </div>
<!–#_
                    }
                    else
                    {
                        ctx.CurrentItem.csr_ShowLastModifiedTime = true;
                        ctx.CurrentItem.csr_ShowAuthors = true;
                    }

                    if(!Srch.U.e(ctx.CurrentItem.SectionNames))
                    {
                        ctx.CurrentItem.csr_DataShown = true;
_#–>
                        <div class="ms-srch-hover-subTitle"><h3 class="ms-soften">_#= $htmlEncode(Srch.Res.hp_SectionHeadings) =#_</h3></div>
<!–#_
                        var sectionNames = Srch.U.getArray(ctx.CurrentItem.SectionNames);

                        var sectionIndexes = Srch.U.getArray(ctx.CurrentItem.SectionIndexes);
                        if(!Srch.U.n(sectionIndexes) && sectionIndexes.length != sectionNames.length)
                        {
                            sectionIndexes = null;
                        }

                        var hitHighlightedSectionNames = Srch.U.getHighlightedProperty(id, ctx.CurrentItem, "sectionnames");
                        if(!Srch.U.n(hitHighlightedSectionNames) && hitHighlightedSectionNames.length != sectionNames.length)
                        {
                            hitHighlightedSectionNames = null;
                        }

                        var numberOfSectionsToDisplay = Math.min(Srch.SU.maxLinesForMultiValuedProperty, sectionNames.length);
                        var sectionsToDisplay = new Array();

                        var usingHitHighlightedSectionNames = Srch.SU.getSectionsForDisplay(
                            hitHighlightedSectionNames,
                            numberOfSectionsToDisplay,
                            sectionsToDisplay);

                        for(i = 0; i < sectionsToDisplay.length; ++i)
                        {
                            var index = sectionsToDisplay[i];
                            if(Srch.U.n(index))
                            {
                                continue;
                            }

                            var tooltipEncoded = $htmlEncode(sectionNames[index]);

                            var htmlEncodedSectionName = "";
                            if(usingHitHighlightedSectionNames)
                            {
                                htmlEncodedSectionName = hitHighlightedSectionNames[index];
                            }
                            else
                &nbs
p;           {
                                htmlEncodedSectionName = tooltipEncoded;
                            }
_#–>
                            <div class="ms-srch-hover-text ms-srch-ellipsis" id="_#= $htmlEncode(id + HP.ids.sectionName + i) =#_" title="_#= tooltipEncoded =#_">
<!–#_
                                if(!Srch.U.n(sectionIndexes) && sectionIndexes.length >= i && !Srch.U.e(sectionIndexes[index]) && wacurlExist)
                                {
                                    var encodedSectionIndex = "&wdparaid=" + $urlKeyValueEncode(sectionIndexes[index]);
_#–>
                                    <a clicktype="HoverSection" linkIndex="_#= $htmlEncode(i) =#_" href="_#= $urlHtmlEncode(ctx.CurrentItem.ServerRedirectedURL + encodedSectionIndex) =#_" target="_blank">
                                        _#= htmlEncodedSectionName =#_
                                    </a>
<!–#_
                                }
                                else
                                {
_#–>
                                    _#= htmlEncodedSectionName =#_
<!–#_
                                }
_#–>
                            </div>
<!–#_
                        }
                    }
_#–>
                    _#= ctx.RenderBody(ctx) =#_
                </div>
                <div id="_#= $htmlEncode(id + HP.ids.actions) =#_" class="ms-srch-hover-actions">
                    _#= ctx.RenderFooter(ctx) =#_
                </div>
            </div>
<!–#_
            if(!Srch.U.n(ctx.CurrentItem.ServerRedirectedEmbedURL)){
                AddPostRenderCallback(ctx, function(){
                    HP.loadViewer(ctx.CurrentItem.id, ctx.CurrentItem.id + HP.ids.inner, ctx.CurrentItem.id + HP.ids.viewer, ctx.CurrentItem.id + HP.ids.preview, ctx.CurrentItem.ServerRedirectedEmbedURL, ctx.CurrentItem.ServerRedirectedPreviewURL);
                });
            }
_#–>
        </div>

This will get the "View File" link to display for the file.  You can then click on it to have the file open in a new browser window and the users can now view the files!

Enjoy!
Chris

Office Web Apps and WOPI Binding at Site Collection Level

If you haven't made it too far into OWA land, then let me forewarn you about some things.  There are two version of Excel Services.  The one that comes with SharePoint and the one that comes with OWA.  The truth is:

You can have one and only one!  – (update:  I should have added here, "in certain areas" to get the point across)

UPDATE: Evidentally, some think it ok and acceptable, to view excel reports through the  excel services web part (with the limited viewing area) rather than the more straight forward way of just viewing it directly in the browser.  I'm not one of those people.

You must decide which one you want.  In our upcoming book, I point out the various difference between the two versions.  The net net is if you want advanced Business intelligence features, you must use Excel Services.  If you want advanced viewing and editing, you must use OWA.  You cannot target web apps, content databases or site collections for the decision.  It is a global farm setting decision.

I find this unacceptable to the community as a whole and I can see a service pack coming that enables us to configure this at least on a web application level and if possible on a site collection basis too.

That being said, it is true, as pointed out by Bryan Hart on my Facebook page, that you can disable the "Viewing" action to allow Excel Services to view the files, but allow editing to be done by OWA.  This is done with the New-SPWOPISupressionSetting cmdlet.

At some point, this will all change with new features and versions and hopefully work much better than it does now.

Chris