A lot of people that are using SharePoint 2007 (WSS or MOSS) for collaboration have either enabled self service site creation in which they allow their end-users to create a page using the scsignup.aspx page or they have some process in place in which an IT administrator creates site collections for their users. Usually companies go the later route due to limitations with the self service site creation process; specifically, you cannot have the site created in a specific database, there's no way to filter the templates available, and there's no obvious way to lock the functionality down to a specific group of users though once you figure it out it's pretty easy (see Gavin's post on the subject: http://blog.gavin-adams.com/2007/09/13/restricting-self-service-site-creation/).

To get around all of these issues and still "empower the end-user" it is necessary to create a custom application page which can handle the creation of the site collection and enforce any custom security or business constraints. At first glance this process would appear really straightforward - the SPSiteCollection class, which you can get to via the Sites property of the SPContentDatabase or SPWebApplication objects has a series of Add methods that can be used to create your site collection. If you use the collection from the SPWebApplication object then the site will be placed in the database with the most room (sort of); conversely, using the SPContentDatabase's version allows you to create the site collection in the specific database.

But here's the rub: the account creating the site collection, via either of these approaches, must have the appropriate rights to update the configuration database. Obviously your users aren't going to have the appropriate rights so you might think you could use SPSecurity.RunWithElevatedPriviliges (RWEP). Unfortunately this won't work either because unless you are running the page via the SharePoint Central Administration site (SCA) then your application pool identity will also not have the appropriate rights (at least it shouldn't if you've configured your environment correctly). Your next thought might be to create a timer job and run the site creation code within that job because you know your timer service account runs as a farm administrator. However, you now face the same issue as your calling account must also have rights to update the configuration database in order to create the timer job.

There's a few different ways around this problem, each with their own pros and cons:

Grant your application pool accounts appropriate rights to the configuration database. This approach is not recommended as you are violating the concept of least privileges and potentially exposing sensitive information and risking corruption if your application pool should become compromised.

Create a custom windows service that runs as the farm account and uses .NET remoting to communicate tasks. If you think you'll have lots of operations requiring privileged access then this is potentially a good way to go, but it introduces are high degree of complexity and requires an installer to be run on every server in the farm. SharePoint uses this approach with its implementation of the "Windows SharePoint Services Administration" service (SPAdmin). The OOTB scsignup.aspx page uses this service to handle the creation of the site collection and thus get around the security restrictions. Unfortunately there's no way for us to leverage this service by having our own code run using it (like we can with custom timer jobs and the SPTimerV3 service).

Create a virtual application under the _layouts folder of each web application and have it run using the SCA application pool. Using this approach you can put the site collection creation application page under the virtual application and thus get the credentials required to edit the configuration database. The problem with this approach is that you once again must touch not only every server but every web application, which defeats the purpose of using WSP packages for solution deployment.

Direct all site collection requests to an application page under the SCA site and pass in target values. This approach gets around a lot of the issues described above (simple to deploy, runs with an account having the appropriate permissions, etc.). The problem is that you must now expose your SCA site to everyone and you must grant the "Browse User Information" right to everyone.

Call a web service running under the SCA's _layouts folder. The nice thing about this approach is that it is simple to deploy (standard WSP deployment from a single server updates all existing servers and any new servers), easy to create and debug, and simple to maintain. The only downside is that it requires that your WFE servers be able to access the SCA web site. The upside is that you don't have to expose this to everyone - just the WFE servers, and you don't need to grant the "Browse User Information" right as your application pool accounts should have the appropriate rights already. You can also get around high availability issues by having the SCA site run on each server (see Spence's article on high availability of SCA: http://www.harbar.net/articles/spca.aspx).

For my purposes the last approach seems the best approach though you may find cause to use one of the others based on your specific business needs. So with that, how would I actually develop this solution?

The first thing I did was to look at the scsignup.aspx page and copy it over to a custom WSP solution project which I created initially using STSDEV. I then tweaked this page by changing the base class to be a custom class that I'll create and I also switched out the master page to use the simple.master as I didn't want navigational elements showing up (you may want to use your own custom master page to preserve your company brand). Finally I added some additional code to handle the displaying of the welcome menus in the top right corner. Here's the finished ASPX page which I named CreateSite.aspx and put under the "RootFiles/TEMPLATE/Layouts/SCP" folder:

So that was the easy part - we basically just tweaked a copy of an existing file. The next step is to create the code behind file which I called CreateSitePage.cs. To create this file initially I used Reflector to see what was being done in the SscSignupPage class and tried to leverage some of the information from there - this saved me some time in creating properties and figuring out how to deal with the site directory. Ultimately though I had to change a lot of stuff so what I ended up with only looks like the OOTB class on the surface but in reality is quite different. You can see the completed class below:

// We have all our data gathered up so now do the actual work...using (SPLongOperation operation = new SPLongOperation(this)) { operation.LeadingHTML = "Create Site Collection"; operation.TrailingHTML = string.Format("Please wait while the \"{0}\" site collection is being created.", Server.HtmlEncode(siteTitle)); operation.Begin();

// The call to the web service has to run as the process account - otherwise we'd need to grant the// calling user the "Browse User Information" rights to the Central Admin site which we don't want. SPSecurity.RunWithElevatedPrivileges(delegate { Logger.WriteInformation(string.Format("Calling web service to create site collection \"{0}\"", siteUri.OriginalString));// We use a Web Service because neither the user nor the process account will have rights to update// the configuration database (so they can't create the site and we can't even use a timer job so// our best option is to use the Central Admin site as we know that it's app pool account has the// rights necessary). CreateSiteService svc = new CreateSiteService { Url = SPAdministrationWebApplication.Local.GetResponseUri(SPUrlZone.Default).ToString().TrimEnd('/') +"/_vti_bin/SCP/CreateSiteService.asmx", Credentials = System.Net.CredentialCache.DefaultCredentials };// We use the managed path as the hint for the database and the quota. Replace with any other custom logic if needed. svc.CreateSite(rootUri.ToString(), managedPath, managedPath, siteUri.OriginalString, siteTitle, siteDescription, templateLocaleId, ownerLoginName, ownerName, ownerEmail, secondaryContactLogin, secondaryContactName, secondaryContactEmail);

/*************************************************************************************//** Comment out the following if you wish to NOT enable self service site creation. **/if (!webApplication.SelfServiceSiteCreationEnabled) {thrownew SPException(SPResource.GetString("SscIsNotEnabled", newobject[0])); }/*************************************************************************************/

/*************************************************************************************//** Uncomment the following if you wish to require the user belong to a specific **//** SharePoint Group in the current site (or replace with other custom logic). **///if (!SPContext.Current.Web.SiteGroups["GROUPNAME"].ContainsCurrentUser)// SPUtility.SendAccessDeniedHeader(new UnauthorizedAccessException());/*************************************************************************************/

The bulk of the code is simply dealing with data validation and storage. It can take in several querystring values to pre-populate data and these values must be stored in ViewState for use during postback processing. The critical piece is within the BtnCreate_Click event handler in which I'm using the RWEP method to call a custom web service to actually create the site. Note that I'm also checking to make sure that self service site creation is enabled - you may decide to actually remove this check and disable self service site creation thus preventing the user of the scsignup.aspx page and forcing users to utilize this custom page (I normally disable self service site creation and would thus remove the code in the OnLoad event handler which throws an exception if not enabled.

The next thing I need to create was the actual web service. This was a bit of a pain because you have to do some rather silly stuff to get the wsdl and disco files generated and then convert them to ASPX pages. You can see the web service code below:

As you can see there's not much there. I'm simply calling a CreateSite method in a custom utility class. Note that you also need the asmx file and wsdl and disco files - all of which I placed in a subfolder under the ISAPI folder.

The Utilities class is the core piece of code that actually creates the site collection and includes some logic to figure out what content database and quota to use. These last two pieces are critical - in the BtnCreate_Click event handler I'm passing in a "databaseHint" and "quotaHint" string variables which I'm setting to be the managed path. What this means is that the code will use this "hint" to search through all the content databases and quotas and if it finds a match (using a containment check) then it will use the first found match to create the site. If no database is found using the hint then it uses the SPWebApplication's Sites property to create the site, thus letting SharePoint pick the best fit. If no quota template is found then it uses the default quota template for the web application. You can see the Utilities code below:

// If a new managed path is added it will be necessary to either add a corresponding content// database to the web application (must contain the managed path name in the content db name)// or alternatively you must add code as shown in the comments below to force the use of an// existing content database (it is recommended to add a new content database for every managed path// rather than the approach below).//if (databaseHint.ToLower() == "path2")// databaseHint = "path1";

/// <summary>/// Gets the quota template that matches the specified prefix name./// </summary>/// <param name="webApp">The web app.</param>/// <param name="quotaHint">Name of the prefix.</param>/// <returns></returns>publicstatic SPQuotaTemplate GetQuotaTemplate(SPWebApplication webApp, string quotaHint) {// If a new managed path is added it will be necessary to either add a corresponding quota// template to (must contain the managed path name in the quota template name)// or alternatively you must add code as shown in the comments below to force the use of an// existing quota template (it is recommended to add a new quota template for every managed path// rather than the approach below).//if (quotaHint.ToLower() == "path2")// quotaHint = "path1";

There is quite a bit of code for the complete solution but once you get through it all you'll realize that there's really not much going on. The main issue I'm addressing with the current implementation is the ability to choose a content database and quota template based on a managed path - this can be extremely helpful for creating collaboration sites with different DR and performance requirements. As I hope you can see, once you have this code in place you can easily further customize it to restrict what users can create sites or even hide the site templates picker and use some other field to determine which template to use.

As I mentioned previously, you can also pass in several querystring parameters to preset some of the most of the fields thus reducing user input. The following table describes each of the supported parameters:

Parameter Name

Description

Example Usage

Data

Allows the passing of arbitrary data through the site creation process. The provided string value is appended to the return URL as a querystring parameter named Data.

Indicates whether an entry in the site directory is required. Valid values are "True" or "False".

(see above)

EntryReq

The site directory entry requirement specifies whether all, none, or at least one of the fields is required. Valid values are 0=none, 1=at least one category, 2=all categories.

(see above)

You can download a zip file containing the complete Visual Studio 2008 Solution and a deployable WSP file from my downloads page or just click here. Note that I've removed the STSDEV dependency from the project and, though it looks like the STSDEV file structure, it is not using it. You're free to download this code and modify it to your hearts content - just don't expect me to support it .

This one took me a minute to figure out what was involved as the disassembled code was a bit of a mess. After spending some time with it I realized it was actually quite simple - just a matter of setting some properties on the root SPWeb object. The core code, which identifies the properties that need to be set, is shown below:

1:/// <summary>

2:/// Writes to the web property bag.

3:/// </summary>

4:/// <param name="site">The site containing the site directory.</param>

I'm starting to work on getting our Site Directory configured and the first thing I noticed after performing my test upgrade was there were tons of links in the site directory to dead sites. This is because in SPS2003 there was no mechanism (that I'm aware of at least) to clear out dead items in the list. With MOSS there's now a timer job that can be configured to clean up the site directory.

I decided that I wanted to set this up as part of my upgrade script (which of course meant another new command which I called gl-setsitedirectoryscanviewurls). Setting this up can be done pretty easily via the browser by using the central admin tool (Central Administration > Operations > Site Directory Links Scan). I took all of the code from what I disassembled using Reflector (see One of the things I needed my upgrade script to do was to set the master site directory. This can be done easily enough using the central admin tool (Central Administration > Operations > Site Directory Settings).

I took most of my code from what I disassembled using Reflector (Microsoft.SharePoint.Portal.SiteAdmin.LinksCheckerJobSettings). I decided that I've grown tired of trying to work around the fact that Microsoft has not made more methods and constructors public and so I decided to just use reflection to mimic what the LinksCheckerJobSettings class does thus avoiding a lot of headaches and shortening my development time considerably (of course there's always the risk that MS changes these methods but I feel safe with these particular objects). Basically all I'm doing is instantiating a LinksCheckerJob object and setting the appropriate values.

One of the things I needed my upgrade script to do was to set the master site directory. This can be done easily enough using the central admin tool (Central Administration > Operations > Site Directory Settings). I took most of my code from what I disassembled using Reflector. The code itself just grabs a PortalService object which it then uses to set the settings. You also need to get an SPSite and SPWeb object in order to set the settings correctly. The syntax of the command can be seen below.