Speaking at SharePoint Saturday Charlotte

I am speaking at the Charlotte SharePoint Saturday event tomorrow.  Below is my abstract and slide deck if you are interested.  If you are a reader of my blog and are going to the event, please stop by and say hello.

Session Title:  Skanska’s Partner Portal and External Document Management System

Session Summary:  Skanska needed a single, consistent place for all project teams to access the important tools they needed on a day to day basis.  A project document management system which allowed internal and external collaboration and an easy to use landing page were keys to success.

Session Topic:  Allowing users to find tools and documents they need to get their job done can be difficult in an enterprise where every project team has different needs and requirements.  Skanska accomplished this by creating a site template which had easy to use links to different enterprise tools and created an effective project document management system where both external and internal parties contributed content.  We will be demoing an early release product while discussing the following pieces of functionality:

  • Managed Metadata
  • Content types and the syndication hub
  • Content Organizer
  • SkyDrive Pro
  • Email enabled lists and libraries
  • Custom Solutions
    • Metadata Based Security
    • Batch Edit (Codeplex)
    • Download as a Zip

Here is the link to the presentation.

Resources for setting up Kerberos Authentication in SharePoint

I recently had a colleague ask me about Kerberos authentication in SharePoint.  They were attempting to get around the infamous NTLM double-hop issue.  Below is the list of resources which I use when getting Kerberos working in SharePoint.

SharePoint 2010 Kerberos document

  • Word Document with step by step instructions on how to setup Kerberos in SharePoint and SQL

Kerbtray

  • System Tray Utility that displays your current Kerberos tickets.  It helps to make sure that you are actually logging in using Kerberos and not ntlm.

Delegconfig

Fiddler2

  • This proxy based packet sniffer is absolutely fantastic for troubleshooting authentication and other SharePoint/website issues.  It will also decrypt HTTPS traffic if you enable it in the settings and add the cert.

Wireshark

  • If fiddler2 isn’t showing you everything this low level packet sniffer will show you everything but it NOT decrypt HTTPS traffic.

It may be included in the documentation above but if you are publishing SharePoint externally through your firewall you will need to open a few ports on your firewall to point to your AD domain controllers.

Also, with SharePoint 2013, claims based authentication is the preferred authentication mechanism.  SP 2013 does include a nice authentication system called windows claims which will work with NTLM and Kerberos but if you are running any custom code in SharePoint, you might need to change it to work correctly with claims auth.

Process Drop Off Library Items Programmatically

I was working on a solution at work where they wanted to tag and approve all documents submitted to the site.  SharePoint 2010’s Content Organizer seemed like it would meet their needs.  I turned on approval for the drop off library and made the default content type be document.  That way when a external user submitted a document, they would just hit save and then the internal user who is approving it, would change it’s content type, fill out the metadata, and approve it.  Then the content organizer routing kicks in and puts it in the correct location.

This all worked great when working with a single document.  What didn’t work well is if you have several documents that needed to have the same metadata applied, approved, and routed.  I turned to Batch Edit on CodePlex for this piece.  I had used it before and with a few tweaks it worked great.  I ran into two issues though in this scenario.

  1. Batch Edit didn’t allow me to edit the content types, so we still had to go to each item individually to change it’s content type
  2. Batch Edit had no way to approve the items
  3. When saving an item programmatically in SharePoint, the routing rules did not process the item until the timer job ran (typically set to run at night once a day).

I wrote some code to fix #1 which was pretty straight forward and I can include the code here on the blog if someone leaves a comment saying they want it.  #2 involved adding a second button to the page that says save and approve and a little extra code on the save to check if that button was pressed and approve the item.  #3 was a lot harder than I thought it would be to get resolved.

Initially I was using OfficialFileCore.SubmitFile().  After realizing there were some additional permissions needed (Records Center Web Service Submitters SP group) I actually got it working.  It seems that OfficialFileCore.Submit() is mainly used to put a file INTO the drop off library and run the routing rules against that file.  The issue was that the files I needed to push through were already in the drop off library, and when using the OfficialFileCore.SubmitFile(), it made an extra copy of those files in the drop off library with a random name that I would somehow need to figure out and delete.  This is not exactly what I wanted and seemed overly complicated.

Remember that timerjob that runs nightly to process items in the drop off library and send emails out about items missing metadata, it’s called the Content Organizer Processing job  (Microsoft.Office.RecordsManagement.Internal.RecordsRepositoryJobDefinition).  I reflectored it and found it was calling an internal method named ProcessWaitingFiles which was looping through all of the files and calling another internal method named ProcessWaitingFile(SPListItem, bool).  This second one was really interesting to me since it only required the SPListItem needing to process and a bool which is whether or not moderation is enabled on the library.  Using reflection I was able to call this method and my files in the drop off library got routed just like I was doing them individually.

Here’s the code:

private void ProcessDropOffLibraryItem(SPWeb web, SPListItem item, bool listRequiresModeration)
{
	EcmDocumentRoutingWeb routingWeb = new EcmDocumentRoutingWeb(web);
	Type clsSPRequest = typeof(EcmDocumentRoutingWeb).Assembly.GetType("Microsoft.Office.RecordsManagement.RecordsRepository.EcmDocumentRoutingWeb", true);
	System.Reflection.MethodInfo processWaitingFile = clsSPRequest.GetMethod("ProcessWaitingFile", BindingFlags.Instance | BindingFlags.NonPublic);

	try
	{
		object result = processWaitingFile.Invoke(routingWeb, new object[] { item, listRequiresModeration });
	}
	catch (System.Reflection.TargetInvocationException ex)
	{
		string message = string.Format("Unable to route file {0}: {1}", item.File.ServerRelativeUrl, ex.Message);
		SPCriticalTraceCounter.AddDataToScope(67, "SP2010 BatchEdit", 1, message + ": " + ex.StackTrace);
	}
}

SharePoint DNS name considerations when using anonymous access

Last year we migrated our external SharePoint farm from SharePoint 2007 to SharePoint 2010. This envrionment has anonymous access turned on for the web application and certain sites have anonymous turned on. We’ve been having issues when accessing these sites with anonymous turned on where it would show us as being signed out for one request and then signed in for the next. See the video here to see what I’m talking about (please ingore the fact that the picture is not working, that’s an unrelated issue…).

We ended up opening a case with Microsoft and they were able to finally figured out what’s going on. It’s a cookie conflict problem with the WSS_KeepSessionAuthenticated cookie. This is the cookie that get’s set to let SharePoint know that you have already authenticated when on an anonymous site. The reason we have a conflict was because of how our dns names for are SharePoint sites are setup. We have a SP 2007 environment at https://company.com which sets the cookie to be 443 (https port #). Our anonymous accessible SP 2010 environment is at https://partners.company.com which is the one with the issues. This environment sets the cookie to be a guid.

Since partners.company.com is a sub-domain of company.com, both the guid and the 443 get sent to partners.company.com for the same cookie name because of sub-domain cookie sharing. I think the SP code in SP 2010 is only looking at the first value when checking for the cookie in the request and sometimes 443 is first and other times the guid is first in the cookie’s namevaluecollection. Thus it shows a user as being logged out when the 443 happens to be first in the namevaluecollection and logged in when the guid is first.

This never showed up before with us and SP 2007 because we used SSL everywhere so the cookie was being set to 443 for all of the SharePoint web applications. Now that SP 2010 uses the web application guid I see this being a bigger problem. Our ultimate solution is to change the dns names for our SharePoint sites. Until then, I was playing around with an HttpModule to “fix” the issue on our SP 2010 environment. Unfortunately, when I tried to remove the cookie that was not a guid, this also causes a race condition with the SharePoint HttpModule which checks the cookie and caused the same behavior even when the correct cookies were being sent. So the thought was to create a HttpModule for SP 2007 that would remove the cookie after it was being set because we aren’t using anonymous in our SP 2007 environment. Unfortunately, this didn’t work either because we use blob caching which exited out of the pipeline before I could remove the cookie. So my final code, which is below, was to always expire the cookie and hopefully, a later request will expire any that the blob cache sets.

using System;
using System.Collections.Generic;
using System.Text;
using System.Web;

public class RemoveAuthCookie : IHttpModule
{

	/// <summary></summary>
	/// Initializes the module, and registers for application events.
	/// 
	public void Init(HttpApplication context)
	{
		context.PreRequestHandlerExecute += new EventHandler(context_PreRequestHandlerExecute);
		context.PostAuthenticateRequest += new EventHandler(context_PostAuthenticateRequest);
	}

	void context_PostAuthenticateRequest(object sender, EventArgs e)
	{
		HttpApplication app = sender as HttpApplication;
		RemoveCookie(app);
	}

	void context_PreRequestHandlerExecute(object sender, EventArgs e)
	{
		HttpApplication app = sender as HttpApplication;
		RemoveCookie(app);
	}

	void RemoveCookie(HttpApplication app)
	{
		if (app == null) return;
		if (app.Response == null) return;
		if (app.Response.Cookies == null) return;

		try
		{

			if (app.Response.Cookies[&quot;WSS_KeepSessionAuthenticated&quot;] == null)
			{
				//WSS_KeepSessionAuthenticated cookie not found
			}
			else
			{

				app.Response.Cookies[&quot;WSS_KeepSessionAuthenticated&quot;].Expires = DateTime.Now.AddDays(-1D);
				//WSS_KeepSessionAuthenticated cookie EXPIRED
			}

			if (app.Response.Cookies[&quot;MSOWebPartPage_AnonymousAccessCookie&quot;] == null)
			{
				//MSOWebPartPage_AnonymousAccessCookie cookie not found
			}
			else
			{
				app.Response.Cookies[&quot;MSOWebPartPage_AnonymousAccessCookie&quot;].Expires = DateTime.Now.AddDays(-1D);
				//MSOWebPartPage_AnonymousAccessCookie cookie EXPIRED
			}                
		}
		catch { }
	}

	/// <summary></summary>
	/// Disposes of the resources (other than memory) used by the module.
	/// 
	public void Dispose()
	{
	}
}

So, this is not a fun situation to be in, so please keep this in mind when you are architecting your SharePoint solutions, a little thing such as the url used to access the site can be a hard thing to change later.

Option to Show the SharePoint 2010 ribbon when hidden by default

At work we’ve been working on a new intranet project with SharePoint 2010 being our platform of choice. This site is mainly a view only type of site but we also wanted users to have the ability to create alerts and other similar activities. One of the requirements of this project was to remove the ribbon on every page. This brought complications for SP 2010 because so much is done in the ribbon. We decided to hide the ribbon by default and show a caret (or chevron) at the upper right of the screen to allow the ribbon to be shown and hidden by all users.

There are many solutions I’ve seen on the interwebs around hiding the ribbon for some and not hiding it for others so they were a good starting point for us but I couldn’t find anyone talking about having the ribbon being optional for everyone. This brought a few challenges such as keeping the ribbon shown on postbacks and displaying the ribbon when clicking on a list webpart.

The first challenge was easy to accomplish. Using a cookie to set whether the user currently has the ribbon open or closed allows the ribbon to “remember” that setting as a user is browsing and editing items in SP.

 var val = 'False';
 try { val = GetCookie('ShowHideRibbon'); } catch (err) { }
 var showRibbon = document.getElementById('ShowRibbon');
 var hideRibbon = document.getElementById('HideRibbon');
 var ribbon = document.getElementById('s4-ribbonrow');

 if (val == 'True') {
  ribbon.style.display = 'block'; ;
  if (showRibbon != null)
   showRibbon.style.display = 'none';
  if (hideRibbon != null)
   hideRibbon.style.display = 'block';
 }
 else {
  if (hideRibbon != null)
   hideRibbon.style.display = 'none';
  if (showRibbon != null)
   showRibbon.style.display = 'block';
 }

The second issue was more difficult and wasn’t initially obvious. The issue was that in SP 2010, when you click on a list view webpart it automatically activates the ribbon. This is great when using the checkboxes to select items or documents. Normal behavior is when the ribbon is activated it will hide the site level breadcrumbs / title bar and the top navigation to use that space for the ribbon itself. But with the ribbon being hidden, this causes the whole top part of the page to disappear including our caret to reopen the ribbon.

This resulted in a very bad user experience. I spent a lot of time debugging JavaScript trying to find the right SP JS function to “override” in order to tap into this action and unhide the ribbon first before the ribbon is activated. I got no where asking the google and ended up searching all of the SharePoint JS debug files and finally finding the function OnRibbonMinimizedChanged in init.js.

 function OnRibbonMinimizedChanged(ribbonMinimized)
 {ULSxSy:;
  var ribbonElement=GetCachedElement("s4-ribbonrow");
  var titleElement=GetCachedElement("s4-titlerow");
  if (ribbonElement)
  {
   ribbonElement.className=ribbonElement.className.replace("s4-ribbonrowhidetitle", "");
   if(titleElement)
   {
    titleElement.className=titleElement.className.replace("s4-titlerowhidetitle", "");
    if (ribbonMinimized)
    {
     titleElement.style.display="block";
    }
    else
    {
     titleElement.style.display="none";
    }
   }
  }
...

I overrode the function with my own where I unhide the ribbon and then called the original function in init.js.

 function SetupEventHandler_HideShowRibbon() {
  if (original_functionEvent == null) {
   original_functionEvent = OnRibbonMinimizedChanged;
  }
  OnRibbonMinimizedChanged = OnRibbonMinimizedChanged_Delegate;
 }

 function OnRibbonMinimizedChanged_Delegate(ribbonMinimized) {
  if (IsRibbonHidden_HideShowRibbon() &&
   false == ribbonMinimized) {
   showRibbon__HideShowRibbon();
  }
  original_functionEvent(ribbonMinimized);
 }

I put all of my JS code into a user control which I added to my masterpage inside a SP delegate control. This allows me to use features to swap this code out with something different. For instance you could have a user control which can hide the ribbon by default and only show the caret / chevron when a user has certain permissions.

<%@ Control Language="C#"    compilationMode="Always" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="uc" TagName="ShowHideRibbonCore" src="~/_controltemplates/Custom/ShowHideRibbon.ascx" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<!-- Show / Hide Ribbon -->
<Sharepoint:SPSecurityTrimmedControl ID="SecurityTrimHideShowRibbonCurrentItem" runat="server" PermissionsString="AddAndCustomizePages" PermissionContext="CurrentItem">
    <uc:ShowHideRibbonCore ID="HideShowRibbonCoreCurrentItem" runat="server"/>
</Sharepoint:SPSecurityTrimmedControl>
<Sharepoint:SPSecurityTrimmedControl ID="SecurityTrimHideShowRibbonCurrentWeb" runat="server" PermissionsString="AddAndCustomizePages" PermissionContext="CurrentSite">
    <uc:ShowHideRibbonCore ID="HideShowRibbonCoreCurrentSite" runat="server"/>
</Sharepoint:SPSecurityTrimmedControl>

 

The code that we ended up doing is below. We hide the ribbon using CSS in the user control and then output the javascript.

<%@ Control Language="C#"    compilationMode="Always" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="uc" TagName="ShowHideRibbon" src="~/_controltemplates/Custom/ShowHideRibbon.ascx" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<!-- Hide Ribbon -->
<style type="text/css">
#s4-ribbonrow
{
    display:none;
}
</style>
<uc:ShowHideRibbonCore ID="HideShowRibbon" runat="server"/>

And here is my full code that I put in the user control ShowHideRibbon.ascx

<%@ Control Language="C#"    compilationMode="Always" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<!-- Show / Hide Ribbon Core -->
<script type="text/javascript">
    if (testForRibbon_HideShowRibbon == null) {
        var original_functionEvent = null;

        var testForRibbon_HideShowRibbon = function () {

            if (IsRibbonHidden_HideShowRibbon()) {
                ExecuteOrDelayUntilScriptLoaded(SetupEventHandler_HideShowRibbon, "init.js");

                var val = 'False';
                try { val = GetCookie('ShowHideRibbon'); } catch (err) { }
                var showRibbon = document.getElementById('ShowRibbon');
                var hideRibbon = document.getElementById('HideRibbon');
                var ribbon = document.getElementById('s4-ribbonrow');

                if (val == 'True' ||
                   (IsRibbonHidden_HideShowRibbon() &&
                    IsTitleRowHidden_HideShowRibbon())) {
                    ribbon.style.display = 'block';
                    if (showRibbon != null)
                        showRibbon.style.display = 'none';
                    if (hideRibbon != null)
                        hideRibbon.style.display = 'block';
                }
                else {
                    if (hideRibbon != null)
                        hideRibbon.style.display = 'none';
                    if (showRibbon != null)
                        showRibbon.style.display = 'block';
                }
            }
        }

        function IsRibbonHidden_HideShowRibbon() {
            return IsElementHidden_HideShowRibbon('s4-ribbonrow');
        }

        function IsTitleRowHidden_HideShowRibbon() {
            return IsElementHidden_HideShowRibbon('s4-titlerow');
        }

        function IsElementHidden_HideShowRibbon(elementId) {
            var computedStyle = function (el, style) {
                var cs;
                if (typeof el.currentStyle != 'undefined') {
                    cs = el.currentStyle;
                } else {
                    cs = document.defaultView.getComputedStyle(el, null);
                }
                return cs[style];
            }
            var element = document.getElementById(elementId);
            if (element != null &&
				computedStyle(element, 'display') == 'none') {
                return true;
            }
            return false;
        }

        function SetupEventHandler_HideShowRibbon() {
            if (original_functionEvent == null) {
                original_functionEvent = OnRibbonMinimizedChanged;
            }
            OnRibbonMinimizedChanged = OnRibbonMinimizedChanged_Delegate;
        }

        function OnRibbonMinimizedChanged_Delegate(ribbonMinimized) {
            if (IsRibbonHidden_HideShowRibbon() &&
				false == ribbonMinimized) {
                showRibbon__HideShowRibbon();
            }
            original_functionEvent(ribbonMinimized);
        }

        function makeDoubleDelegate_HideShowRibbon(function1, function2) {
            return function () {
                if (function1)
                    function1();
                if (function2)
                    function2();
            }
        }

        function getDocHeight_HideShowRibbon() {
            var D = document;
            return Math.max(
            Math.max(D.body.scrollHeight, D.documentElement.scrollHeight),
            Math.max(D.body.offsetHeight, D.documentElement.offsetHeight),
            Math.max(D.body.clientHeight, D.documentElement.clientHeight)
        );
        }

        function showRibbon__HideShowRibbon() {
            var newHeight = getDocHeight_HideShowRibbon();
            var ribbon = document.getElementById('s4-ribbonrow');
            ribbon.style.display = 'block';
            try {
                SetCookie('ShowHideRibbon', 'True', '/');
            } catch (err) { };
            document.getElementById('ShowRibbon').style.display = 'none';
            document.getElementById('HideRibbon').style.display = 'block';

            newHeight = newHeight - ribbon.offsetHeight;
            var ver = navigator.appVersion;
            if (ver.indexOf("MSIE") != -1) {
                newHeight = newHeight - 4;
            }
            document.getElementById('s4-workspace').style.height = newHeight + 'px';
        }

        function hideRibbon__HideShowRibbon() {
            document.getElementById('s4-ribbonrow').style.display = 'none';
            try {
                SetCookie('ShowHideRibbon', 'False', '/');
            } catch (err) { };
            document.getElementById('HideRibbon').style.display = 'none';
            document.getElementById('ShowRibbon').style.display = 'block';
            var newHeight = getDocHeight_HideShowRibbon();
            var ver = navigator.appVersion;
            if (ver.indexOf("MSIE") != -1) {
                newHeight = newHeight - 4;
            }
            document.getElementById('s4-workspace').style.height = newHeight + 'px';
        }

        window.onload = makeDoubleDelegate_HideShowRibbon(window.onload, testForRibbon_HideShowRibbon);

        document.write('<a id="ShowRibbon" href="#" onclick="showRibbon__HideShowRibbon(); return false;"><img src="/_layouts/images/downarrow.png" title="Show Ribbon" border="0"/></a>');
        document.write('<a id="HideRibbon" href="#" onclick="hideRibbon__HideShowRibbon(); return false;"><img src="/_layouts/images/uparrow.png" title="Hide Ribbon" border="0"/></a>');
    }
</script>
<style type="text/css">
#ShowRibbon
{
	position:fixed;
	top:0;
	right:15px;
	display: none;
	z-index: 100;
}

#HideRibbon
{
	position:fixed;
	top:45px;
	right:15px;
	display: none;
	z-index:100;
}
</style>

Update 4-6-2012

I realized there are instances when visiting a page (like dispform.aspx) where the ribbon is supposed to be open by default but instead showed the same bad behavior from above. I’ve updated my code above to now check if s4-titlerow is hidden in addtion to the s4-ribbonrow. If so, I unhide the ribbon. But I do not cookie this change. The only time this app “remembers” your setting is if you actually click the up or down arrow at the top right.