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
                            {
                                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

SharePoint’s Navigation Struggles – A Bit of History

SharePoint has always struggled with navigation.  Every customer I have been to we have either torn out the navigation completely, or had to customize it in some way to achieve their goals.  SharePoint 2013 is no exception to these navigation woes, and if anything, we have taken a step backwards. In looking at what customer's want, I have found there are three types of navigation:

  • Global-global – this navigation exists across the top of all sites in a SharePoint farm and is exactly the same.
  • Local-global – this navigation is for the site collection navigation and is the same across the site collection
  • Local-local – this navigation is for the site navigation and shows important lists and pages on a site

To be clear, SharePoint has never had "Global-Global" navigation.  It has however had the "Local-global" and "local-local" navigation, with some helper tools for inter-site collection navigation.  In order to fully point out this backwards momentum, let's take a look at some of the previous out of the box UIs:

2007:

 

As you can see we started with some nice tabbed navigation, with the quick launch.  It was nice and simple, we also had the breadcrumb to get us back up to anywhere in the site tree.  However, to really get valuable navigation, you had to enable the
PublishingSite and PublishingWeb features to get the fly out navigation
of subsites:

 

2010:

 

In 2010, we lost the breadcrumb control that was directly above the place holder main area, but we gained the folder icon in above the ribbon.  Again, we have to enable the publishing features to get any real value out of the local-global.

2013:

 

In 2013, the local-global was moved from directly below the ribbon, to inside the ribbon.  We also lost the folder icon.

As you can see, we have lost (removed from the master pages yet still exist in the code base) some great functionality over the years.

It had seemed that Metadata navigation was in many of our minds (based on marketing hype) the solution for the "global-global" navigation problem that SharePoint has had for many years, but alas it is not:

 

Unfortunately, if you have actually tried to implement it, you will run into these sets of errors:

Trying to use the term store more than once:

 

Using Windows PowerShell to set it:

$navSettings.CurrentNavigation.Source = 1;
$navSettings.CurrentNavigation.TermStoreid = new-object System.Guid("6ffccd26-5aba-44a5-83a9-60a468261054");
$navSettings.CurrentNavigation.TermSetId = new-object System.Guid("420d7ef6-6040-4138-8e15-1d04773955ba")
$navSettings.GlobalNavigation.Source = 1;
$navSettings.GlobalNavigation.TermStoreid = new-object System.Guid("6ffccd26-5aba-44a5-83a9-60a468261054");
$navSettings.GlobalNavigation.TermSetId = new-object System.Guid("420d7ef6-6040-4138-8e15-1d04773955ba")
$navSettings.Update()

This gets you the dreaded "Error loading navigation: The Managed Navigation term set is improperly attached to the site":



So where does that leave us?  It means we have to move the Local-Global of 2013 from the ribbon, back to where it was in 2010 (directly below the ribbon) and then implement our own global navigation provider in the ribbon, we also add back the breadcrumb to the content placeholder main:

 

How did I do this?  Well, its not pretty.  Especially when you realize you have to make your own custom master page, not only that, but just about every site definition has some variation on the basic seattle.master.  This shows up in the Search Center and My Site templates.  Which means you pretty much have to implement a custom master page for each one (which I have done in 2013).  Although it is a lot of work, it is well worth it when your customer gives you the thumbs up and is actually able to navigate the sites inside and outside of SharePoint with ease!  A few steps to get here:

  • Create a custom navigation provider that points to a global list with a hierarchy of elements (or where ever)
  • Remove the ribbon navigation (local-global), replace with your navigation provider menu control
  • Put the removed ribbon navigation directly below the ribbon, add the older CSS to give it the top and bottom border
  • Add the breadcrumb directly above the content place holder main area

In a second spin…what does that mean for O365 customers that want to migrate their intranet or anything else they did to SharePoint Online?  Its not good I'm afraid.  You aren't allowed to push your own code, so the navigation providers are out.  That means you won't get global navigation in SharePoint online unless you figure out a way to do it via a custom <DIV> that is populated based on Javascript methods from some data source (preferably from a SPList via REST).  But now you have to implement all the flyout code (unless their is some reliable and simple SDK you can find on the internet to do it and plug into your SP Rest calls and hope that SharePoint CSS doesn't mess with it).   And don't forget about the lovely security in JavaScript that prevents cross domain calls.

One other option I thought about for the global-global was to put it in the first row of the 2013 page.  I had lots of issues with the div tags and flyouts of the basic asp and SharePoint menu's.  Although I'm sure I could figure it out at some point, it ended up being much easier just to put the global-global in the ribbon and move the local-global down.

So, with this little bit of education, hopefully we can get someone in the product team to get serious about providing this type of functionality to meet our customer's needs (global-global) without us having to do anything.  I still think MMS is the way to go, but I'd guess their are issues with doing this in O365 and that's why it was limited to one site collection.

Chris

Content Type Hub publishing in mixed mode sites (14 vs 15) – Upgrade and migration planning

I noticed this post today from Brad Teed…It's a good one!:

http://sharepointsblog.com/2013/05/06/sharepoint-2013-content-type-hub/

I would have assumed that the content types would be the same in either mode, but evidentially not!  There is a check in the code at Microsoft.SharePoint.Taxonomy.ContentTypeSync.Internal.PackageInfo.ValidatePackageVersion().  It checks the hub site collection compatibility level and then checks the current web's site collection compatibility level.  If it doesn't match, then it errors out.  This is a bummer for those that used the Content Publishing Hub in 2010 and must now upgrade all site collections at the same time to "15" in order to get updates from the hub.

However, there is something else interesting here.  The APIs that are being used by the content type hub publishing are the Content Deployment APIs.  So the package that it is checking is a simple export/import package that you would do with anything else.  The thing that jumps out at me, that I would have assumed one could do, is export from 2010 and import into 2013 using those APIS, but it look like that is not a good path to take being that they specifically are denying you from doing this.  This tells me that you should upgrade your older sites to 2013, then do an export, then do an import into 2013.

Chris