Explorer View not working and Managed paths

Since upgrading to Windows Vista in June 2007, I’ve been having a hard time getting explorer view working in our SharePoint 2007 environment. Some of you might have seen this message, "Your client does not support opening this list with Windows Explorer."

In October 2007, a new entry on the SharePoint blog (http://blogs.msdn.com/sharepoint/archive/2007/10/19/known-issue-office-2007-on-windows-vista-prompts-for-user-credentials-when-opening-documents-in-a-sharepoint-2007-site.aspx) was posted which explained the issue. I had been using the fakeproxy workaround for a while (http://www.portalsolutions.net/Blog/Lists/Posts/Post.aspx?ID=27) but still had limited success with it. it seems it would work on some sites but most sites it wouldn’t work. After installing all of the hotfixes related to the issue and installing Vista SP1 which I hoped to fix the issue, I was able ready to give up on Vista.

I calmed down a bit and decided to try to figure this thing out and worst case is to call Microsoft for support. I knew my next step was to get out the packet sniffer (www.wireshark.org) and see if that help shed some light on things. The first thing I noticed when viewing the packets of a working explorer view was that it seems the webdav does a propfind for all directories under the current site you are trying to open in explorer view. For example, if I had a document library at http://site/usa/teams/IT/documents/ then vista would do a propfind at /, /usa, /usa/teams, /usa/teams/IT, and then finally /usa/teams/IT/documents.

But when I tried explorer view on a document library that didn’t work, it would do the propfind at / and then /usa but the server would respond 404 not found.

This was an issue for us because one of our managed paths was usa/teams. So apparently if it’s a normal managed path then sharepoint will respond to the propfind, but if there is a slash (/) in a managed path then sharepoint doesn’t know to respond to the propfind. I ended up having to create a site at the path /usa and that fixed my explorer view issues.

As it turns out, windows xp also does webdav this way, but when the webdav fails in windows xp, frontpage remote procedure call (fprpc) kicks in to display the explorer view but that wasn’t happening on my vista machine.

Using your SharePoint site Master Page on all application pages

So I found this article, http://weblogs.asp.net/soever/archive/2006/11/14/SharePoint-2007_3A00_-using-the-masterpage-from-your-site-in-custom-_5F00_layouts-pages.aspx, which explains how to use your SharePoint site’s master page when you create a custom application page in the _layouts virtual directory. At this point in time I don’t really have any custom application pages yet but I liked the idea of when I theme a site that all pages, including application pages in the _layouts virtual directory, use the same master page. By default all application pages use the application.master master page. After finding this article, http://www.odetocode.com/Articles/450.aspx, and a lot of playing around I believe I have found a solution. I created a HttpModule that on a page’s preinit, it checks to see if the page is in the _layouts virtual directory, and if so, it will assign it the master page url from the current sharepoint site. Here’s my code:

using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Configuration;
using System.Data.SqlClient;
using System.Data;
using Microsoft.SharePoint;

public class ApplicationMasterPage : IHttpModule
{
	public void Init(HttpApplication context)
	{
		context.PreRequestHandlerExecute += new EventHandler(context_PreRequestHandlerExecute);
	}

	void context_PreRequestHandlerExecute(object sender, EventArgs e)
	{
		Page page = HttpContext.Current.CurrentHandler as Page;
		if (page != null)
		{
			page.PreInit += new EventHandler(page_PreInit);
		}
	}

	void page_PreInit(object sender, EventArgs e)
	{
		Page page = sender as Page;
		if (page == null) return;
		if (page.MasterPageFile == null) return;
		HttpContext context = HttpContext.Current;
		string currUrl = context.Request.Url.ToString();
		string basepath = currUrl.Substring(0, currUrl.IndexOf(context.Request.Url.Host) + context.Request.Url.Host.Length);
		//Use RawUrl, otherwise it will always use the root web.
		currUrl = context.Request.RawUrl;
		if (currUrl.ToLower().Contains("/_layouts/") &&
			page.MasterPageFile.ToLower().EndsWith("application.master"))
		{
			currUrl = currUrl.Substring(0, currUrl.ToLower().IndexOf("/_layouts/"));
			SPSite site = null;
			SPWeb web = null;
            try
            {
				site = new SPSite(basepath + currUrl);
				web = site.OpenWeb(currUrl);

				if (String.IsNullOrEmpty(web.MasterUrl) == false)
	            {
					page.MasterPageFile = web.MasterUrl; 
					page.Load += new EventHandler(page_Load);
				}
			}
            finally
            {
                if (web != null)
                {
                    web.Dispose();
                }
                if (site != null)
                {
                    site.Dispose();
                }
            }
		}
	}

	public void Dispose()
	{
	}

	void page_Load(object sender, EventArgs e)
	{
		Page page = sender as Page;
		if (page == null) return;

		try
		{
			ContentPlaceHolder placeHolderTitleBreadcrumb = page.Master.FindControl("PlaceHolderTitleBreadcrumb") as ContentPlaceHolder;
			SiteMapPath contentMap = placeHolderTitleBreadcrumb.FindControl("ContentMap") as SiteMapPath;
			contentMap.CssClass = "ms-hidden";
		}
		catch { }

		try
		{
			ContentPlaceHolder placeHolderSearchArea = page.Master.FindControl("PlaceHolderSearchArea") as ContentPlaceHolder;
			placeHolderSearchArea.Visible = false;
		}
		catch { }
	}

}

Be sure to register the HttpModule in the web.config of your sharepoint site.

This did require me to add extra contentplaceholders into my default.master. Here are the two extra placeholders I had to add:

PlaceHolderPageDescriptionRowAttr
PlaceHolderPageDescriptionRowAttr2

To add these placeholders into my default.master I replaced the following code:

<PlaceHolder id="MSO_ContentDiv" runat="server">
<table id="MSO_ContentTable" width=100% height="100%" border="0" cellspacing="0" cellpadding="0" class="ms-propertysheet">
<tr>
<td class='ms-bodyareaframe' valign="top" height="100%">
<A name="mainContent"></A>
<asp:ContentPlaceHolder id="PlaceHolderPageDescription" runat="server"/>
<asp:ContentPlaceHolder id="PlaceHolderMain" runat="server">
</asp:ContentPlaceHolder>
</td>
</tr>
</table>
</PlaceHolder>

with

<PlaceHolder id="MSO_ContentDiv" runat="server">
<table id="MSO_ContentTable" width=100% height="100%" border="0" cellspacing="0" cellpadding="0" class="ms-propertysheet">
<TR valign="top" <asp:ContentPlaceHolder id="PlaceHolderPageDescriptionRowAttr" runat="server"/> >
<TD class="ms-descriptiontext" width="100%">
<asp:ContentPlaceHolder id="PlaceHolderPageDescription" runat="server"/>
</TD>
</TR>
<TR <asp:ContentPlaceHolder id="PlaceHolderPageDescriptionRowAttr2" runat="server"/>>
<TD ID=onetidMainBodyPadding height="8px"><IMG SRC="/_layouts/images/blank.gif" width=1 height=8 alt=""></TD>
</TR>
<tr>
<td class='ms-bodyareaframe' valign="top" height="100%">
<A name="mainContent"></A>
<asp:ContentPlaceHolder id="PlaceHolderMain" runat="server">
</asp:ContentPlaceHolder>
</td>
</tr>
</table>
</PlaceHolder>

I also had to change my default breadcrumbs code in the default.master to match the application.master. I replaced the following code:

<asp:contentplaceholder id="PlaceHolderTitleBreadcrumb" runat="server">
	<asp:sitemappath id="ContentMap" runat="server" sitemapprovider="SPContentMapProvider" skiplinktext="" nodestyle-cssclass="ms-sitemapdirectional"></asp:sitemappath> &nbsp;
</asp:contentplaceholder>

with

<asp:contentplaceholder id="PlaceHolderTitleBreadcrumb" runat="server">
	<asp:SiteMapPath SiteMapProvider="SPXmlContentMapProvider" SkipLinkText="" NodeStyle-CssClass="ms-sitemapdirectional" runat="server"/>

	<asp:SiteMapPath SiteMapProvider="SPContentMapProvider" id="ContentMap" SkipLinkText="" NodeStyle-CssClass="ms-sitemapdirectional" runat="server"/>
</asp:contentplaceholder>

————————————————————————————————–

Update (02/23/08): I’ve changed my default.master and my httpmodule around a lot since posting this. If you don’t compile a dll you will need to place the ApplicationMasterPage.cs file in a App_Code directory of your root site and in a App_Code directory at C:Program FilesCommon FilesMicrosoft SharedWeb Server Extensions12TEMPLATEIMAGES. You can download my latest files below:

default.master

ApplicationMasterPage.cs

————————————————————————————————–

All of the code and changes ended up with a site settings page that looks like this.

Using Features to enable Drop-Down Menus in Team Sites

I recently was trying to enable the top nav bar to be a drop down menu of all of the sub sites. I found this article http://sharingpoint.blogspot.com/2007/02/wss-v3-drop-down-menus-in-team-sites.html which told me everything I needed to know. Instead of modifying the default.master and the TopNavBar.ascx (for the application.master) to change the SPNavigationProvider to SPSiteMapProvider, I decided I wanted each teamsite to have the option and I’m reluctant from changing SharePoint system files (see http://www.graphicalwonder.com/?p=532), so I put this part into a feature. Here’s my code below:

The first file is Feature.xml

<Feature  Id="22b94164-5348-11dc-8314-0800200c9a66"
          Title="All Sites in Site Collection Navigation"
          Description="Enable all sites in the site collection top navigation bar."
          Version="12.0.0.0"
          Scope="Web"
          Hidden="false"
    DefaultResourceFile="core"
    xmlns="http://schemas.microsoft.com/sharepoint/">
    <ElementManifests>
        <ElementManifest Location="NavigationSiteSettings.xml"/>
    </ElementManifests>
</Feature>

The second file is NavigationSiteSettings.xml

<Elements
    xmlns="http://schemas.microsoft.com/sharepoint/">
    <!-- Top Nav -->
 <Control 
        Sequence="25"
        Id="TopNavigationDataSource"
  ControlClass="System.Web.UI.WebControls.SiteMapDataSource"
  ControlAssembly="System.Web, version=2.0.3600.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
        <Property Name="ID">topSiteMap</Property>        
        <Property Name="SiteMapProvider">SPSiteMapProvider</Property>
        <Property Name="ShowStartingNode">true</Property>
    </Control>
    <HideCustomAction
        Id="TopNav"
        HideActionId="TopNav"
        GroupId="Customization"
        Location="Microsoft.SharePoint.SiteSettings" />
</Elements>

I still needed to modify the default.master and TopNavBar.ascx (you don’t need to edit the TopNavBar.ascx if you are implementing my Using your SharePoint site Master Page on all application pages) so that more than one level is shown. I haven’t figured out a way around that part yet. Does anyone else know a way to override the SharePoint.AspMenu control?

————————————————————————————————————
Update (09/21/07): I have since found out that apparently the SPSiteMapProvider relies on the user having permissions to view the top-level site in the site collection. If the user does not have access to this top-level site, then they will be prompted for authentication and won’t be able to access the site. Using Reflector I was able to find the code where they are finding the top-level site without checking for permissions first.

I ended up having to write my own SiteMapProvider that did the correct security checking and trimming while still allowing access to the site.

On a side note, while I was looking into this issue, I also noticed that the Global Breadcrumbs (which also uses the SPSiteMapProvider) doesn’t do security trimming. So I wrote a SiteMapProvider for the Global Breadcrumbs as well that did security trimming. There doesn’t seem to be a delegate control for the Global Breadcrumbs like there is for the top nav so I couldn’t write a feature to override the default. So I decided to just change my default.master and in combination with Using your SharePoint site Master Page on all application pages will use my custom SiteMapProvider for the Global Breadcrumbs.

————————————————————————————————————
Update (05/19/08): Some people have asked for the source code of my custom site map providers. You can download my latest files below:

AllSitesInSiteCollectionNavTrimmed.cs

AllSitesBreadcrumbsTrimmed.cs

FYI, this may not be the most efficient code and I have noticed some caching issues with it sometimes. Other than that it seems to work great.

Hosting Multiple unique SharePoint Sites on a single server

Recently we upgraded to a new, faster server for our websites. Ever since WSS v3 was released to the web I’ve been running SharePoint on my home server. But now with the new server hosted in Texas, I decided to move our SharePoint site to this server.

There are a few differences between my home server and our new web server. My home server is also my domain controller but I didn’t want to setup a domain on the new web server. The home server really only needs to service our needs, but our web server needs to be able to service the needs of myself and others. Because of these two differences, I needed a way to run multiple sharepoint sites that were totally independent of each other including users. This is normally done in SharePoint using Active Directory Account Create Mode, but like I said, I didn’t want to run a domain. A new feature with WSS V3 is the ability to have forms based authentication with ASP.NET’s membership providers. This was what I needed to do.

I first found this blog entry http://weblog.vb-tech.com/nick/archive/2006/06/14/1617.aspx?harrison on how to setup forms based authentication using a SQL Server membership provider. I was able follow his directions and set this up but there were a few issues. One, these directions assume that you will only be having one membership provider per sharepoint server, which was not the case for me. I needed to have a different membership provider for each sharepoint site on this one server. Second, I had no good way of adding, editing, and deleting users.

To fix the first issue involved doing a little copy and paste action out of the machine.config file into my web.config for the SharePoint site. I needed to change the name of the membership provider and tell this SharePoint site to use my newly named membership provider as the default provider. The reason I have to have a unique name for each SharePoint site is because I need to add every membership provider for all of my sites to the central administrations web.config. My SharePoint site’s web.config membership code looks like the following:

<membership defaultProvider="TheLineberrys_Users">
	<providers>
		<add name="TheLineberrys_Users" type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" connectionStringName="LocalSqlServer" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="true" applicationName="TheLineberrys" requiresUniqueEmail="true" passwordFormat="Hashed" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="7" minRequiredNonalphanumericCharacters="1" passwordAttemptWindow="10" passwordStrengthRegularExpression="" />
	</providers>
</membership>
<profile enabled="true" defaultProvider="TheLineberrys_Profiles">
	<providers>
		<add name="TheLineberrys_Profiles" connectionStringName="LocalSqlServer" applicationName="TheLineberrys" type="System.Web.Profile.SqlProfileProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
	</providers>
</profile>
<roleManager enabled="true" defaultProvider="TheLineberrys_Roles">
	<providers>
		<add name="TheLineberrys_Roles" connectionStringName="LocalSqlServer" applicationName="TheLineberrys" type="System.Web.Security.SqlRoleProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
	</providers>
</roleManager>

Notice that my applicationName is the unique name of my website. This allowed me to have one membership database for all of the SharePoint sites but also be able to have completely seperate users, profiles, and roles.

To fix the second issue I found this blog http://weblogs.asp.net/scottgu/archive/2005/10/18/427754.aspx with sample code. This was good enough to start out with but didn’t provide all of the features I needed. I added this code as a virtual directory under my sharepoint website called profile. I next found http://www.qualitydata.com/products/aspnet-membership/default.aspx which provided a membership manager control that seemed to fit my needs perfectly. I had a few issues getting it working in my profile virtual directory because of SharePoint’s trust level. Eventually I found out that I needed to change the web.config of this virtual directory to full trust to fix my issues. The problem with the Membership Manager is it didn’t allow end users to manage their account, it was more of an administrator tool. So I decided to use a combination of the Profile Sample from Scott Guthrie and the Membership Manager. The Membership Manager will be my profile tools for administrators only, and from the Profile Sample I will use the changepassword and recoverpassword pages for the end users to do password management, and I will also use the createnewwizard as sort of a setup script when I first create a new SharePoint site.

After getting all of this working I needed to make it look pretty. I created my own SharePoint stripped master page based on C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\LAYOUTS\simple.master. Next I setup the colors for the Membership Manager to use SharePoint css classes and also made a profile landing page that based on your permissions and whether or not you are logged in shows the appropriate links.

But this wasn’t quite good enough. The end users would have to remember to go to /profile/ to change their password. This was unacceptable.

I decided to modify some of the SharePoint files. The first file I modified was C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\CONTROLTEMPLATES\Welcome.ascx which is the file that controls the menu items in the welcome menu at the top right of the page when you are logged in. I added the following right above the logout item:

<SharePoint:MenuItemTemplate runat="server" id="ID_ChangePassword"
Text="Change Password"
Description="Change my password used to login"
MenuGroupId="200"
Sequence="250"
UseShortId="true"
ClientOnClickNavigateUrl="/profile/changepassword.aspx"
/>

This now added a Change Password link on the welcome menu of every page for every SharePoint site on the server.

I also changed
C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\LAYOUTS\login.aspx which is the file people see when they are trying to login. I added the following right above the last </asp:Content>:

<p>
<a href="/profile/recoverpassword.aspx">Forgot your password?</a>

So now I have a change password link on every page when someone is logged in and a forgot your password link on the login page.

Another issue I ran into using forms authentication is the search seemed to not be working. I then found this page http://wsssearch.com/formauthentication.html that explains how you have to add another web application that is mapped to your forms authenticated web application so the search crawler can use windows authentication to crawl the website. After following the directions on that page my search was up and going.