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:

  1. 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.
  2. 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).
  3. 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.
  4. 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.
  5. 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:

  1<%@ Assembly Name="Microsoft.SharePoint.ApplicationPages, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>
  2<%@ Page Language="C#" Inherits="Lapointe.SharePoint.SiteCollectionProvisioner.ApplicationPages.CreateSitePage, Lapointe.SharePoint.SiteCollectionProvisioner, Version=1.0.0.0, Culture=neutral, PublicKeyToken=29b13c54ceef5193" MasterPageFile="~/_layouts/simple.master"%>
  3<%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %>
  4<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
  5<%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
  6<%@ Import Namespace="Microsoft.SharePoint" %>
  7<%@ Register TagPrefix="wssuc" TagName="InputFormSection" src="~/_controltemplates/InputFormSection.ascx" %>
  8<%@ Register TagPrefix="wssuc" TagName="InputFormControl" src="~/_controltemplates/InputFormControl.ascx" %>
  9<%@ Register TagPrefix="wssuc" TagName="ButtonSection" src="~/_controltemplates/ButtonSection.ascx" %>
 10<%@ Register TagPrefix="wssuc" TagName="TemplatePickerControl" src="~/_controltemplates/TemplatePickerControl.ascx" %>
 11<%@ Register Tagprefix="wssawc" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
 12<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
 13<%@ Register TagPrefix="wssuc" TagName="Welcome" src="~/_controltemplates/Welcome.ascx" %>
 14<asp:Content ID="Content1" contentplaceholderid="PlaceHolderPageTitle" runat="server">
 15    <SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,scsignup_pagetitle%>" EncodeMethod='HtmlEncode'/>
 16</asp:Content>
 17<asp:Content ID="Content2" contentplaceholderid="PlaceHolderPageTitleInTitleArea" runat="server">
 18    <SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,scsignup_pagetitle%>" EncodeMethod='HtmlEncode'/>
 19</asp:Content>
 20<asp:Content ID="Content3" contentplaceholderid="PlaceHolderPageDescription" runat="server">
 21    <SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,scsignup_pagedescription%>" EncodeMethod='HtmlEncode'/>
 22</asp:Content>
 23<asp:Content ID="Content4" contentplaceholderid="PlaceHolderAdditionalPageHead" runat="server">
 24    <script src="/_layouts/<%=System.Threading.Thread.CurrentThread.CurrentUICulture.LCID%>/commonvalidation.js"></script>
 25
 26<script Language="javascript">
 27
 28function    Visascii(ch)
 29{
 30    return (!(ch.charCodeAt(0) & 0x80));
 31}
 32function Visspace(ch)
 33{
 34    return (ch.charCodeAt(0) == 32) || ((9 <= ch.charCodeAt(0)) && (ch.charCodeAt(0) <= 13));
 35}
 36function stripWS(str)
 37{
 38    var b = 0;
 39    var e = str.length;
 40    while (str.charAt(b) && (Visascii(str.charAt(b)) && Visspace(str.charAt(b))))
 41        b++;
 42    while ((b < e) && (Visascii(str.charAt(e-1)) && Visspace(str.charAt(e-1))))
 43        e--;
 44    return ((b>=e)?"":str.substring(b, e ));
 45}
 46var L_NoFieldEmpty_TEXT = "<SharePoint:EncodedLiteral runat='server' text='<%$Resources:wss,common_nofieldempty_TEXT%>' EncodeMethod='EcmaScriptStringLiteralEncode'/>";
 47function CheckForEmptyField(text_orig,field_name)
 48{
 49    var text = stripWS(text_orig);
 50    if (text.length == 0)
 51    {
 52        alert(StBuildParam(L_NoFieldEmpty_TEXT, field_name));
 53        return false;
 54    }
 55    return (true);
 56}
 57function CheckForEmptyFieldNoAlert(text_orig)
 58{
 59    var text = stripWS(text_orig);
 60    if (text.length == 0)
 61    {
 62        return false;
 63    }
 64    return (true);
 65}
 66var L_WrongEmailName_TEXT = "<SharePoint:EncodedLiteral runat='server' text='<%$Resources:wss,common_wrongemailname_TEXT%>' EncodeMethod='EcmaScriptStringLiteralEncode'/>";
 67function CheckForAtSighInEmailName(text_orig,field_name)
 68{
 69    var text = stripWS(text_orig);
 70    if (!CheckForEmptyField(text_orig,field_name)) return false;
 71    var indexAt = 0;
 72    var countAt = 0;
 73    var countSpace = 0;
 74    var len = text.length;
 75    while(len--)
 76    {
 77        if (text.charAt(len) == '@')
 78        {
 79            indexAt = len;
 80            countAt++;
 81        }
 82        if (text.charAt(len) == ' ')
 83            countSpace ++;
 84    }
 85    if ((countAt == 0) ||
 86        (indexAt == 0) ||
 87        (indexAt == (text.length-1))
 88        )
 89    {
 90        alert(StBuildParam(L_WrongEmailName_TEXT, field_name));
 91        return false;
 92    }
 93    if (countSpace !=0 )
 94    {
 95        alert(L_TextWithoutSpaces1_TEXT + field_name);
 96        return false;
 97    }
 98    return (true);
 99}
100    function _spBodyOnLoad()
101    {
102        try{document.getElementById(<%SPHttpUtility.AddQuote(SPHttpUtility.NoEncode(TxtTitle.ClientID),Response.Output);%>).focus();}catch(e){}
103    }
104    function SiteAddressValidate(source, args)
105    {
106        var stname = stripWS(args.Value);
107        if(IndexOfIllegalCharInUrlLeafName(stname) != -1)
108        {
109            args.IsValid = false;
110            return;
111        }
112        args.IsValid = true;
113    }
114
115</script>
116
117</asp:Content>
118<asp:Content contentplaceholderid="PlaceHolderTitleBreadcrumb" runat="server"><br /></asp:Content>
119<asp:Content contentplaceholderid="PlaceHolderFormDigest" runat="server">
120      <SharePoint:FormDigest runat=server/>
121</asp:Content>
122<asp:Content ContentPlaceHolderID="PlaceHolderGlobalNavigation" runat="server">
123    <table cellpadding="0" cellspacing="0" border="0" width="100%">
124        <tr>
125            <td colspan="4" class="ms-globalbreadcrumb">
126                <span id="TurnOnAccessibility" style="display: none"><a href="#" class="ms-skip"
127                    onclick="SetIsAccessibilityFeatureEnabled(true);UpdateAccessibilityUI();return false;">
128                    <sharepoint:encodedliteral runat="server" text="<%$Resources:wss,master_turnonaccessibility%>"
129                        encodemethod="HtmlEncode" />
130                </a></span><a id="A1" href="javascript:;" onclick="javascript:this.href='#mainContent';"
131                    class="ms-skip" accesskey="<%$Resources:wss,maincontent_accesskey%>" runat="server">
132                    <sharepoint:encodedliteral runat="server" text="<%$Resources:wss,mainContentLink%>"
133                        encodemethod="HtmlEncode" />
134                </a>
135                <table cellpadding="0" cellspacing="0" height="100%" class="ms-globalleft">
136                    <tr>
137                        <td class="ms-globallinks" style="padding-top: 2px;" height="100%" valign="middle">
138                            <div>
139                                <span id="TurnOffAccessibility" style="display: none"><a href="#" class="ms-acclink"
140                                    onclick="SetIsAccessibilityFeatureEnabled(false);UpdateAccessibilityUI();return false;">
141                                    <sharepoint:encodedliteral runat="server" text="<%$Resources:wss,master_turnoffaccessibility%>"
142                                        encodemethod="HtmlEncode" />
143                                </a></span>
144                            </div>
145                        </td>
146                    </tr>
147                </table>
148                <table cellpadding="0" cellspacing="0" height="100%" class="ms-globalright">
149                    <tr>
150                        <td valign="middle" class="ms-globallinks" style="padding-left: 3px; padding-right: 6px;">
151                        </td>
152                        <td valign="middle" class="ms-globallinks">
153                            <wssuc:Welcome id="IdWelcome" runat="server" EnableViewState="false">
154                            </wssuc:Welcome>
155                        </td>
156                        <td style="padding-left: 1px; padding-right: 3px;" class="ms-globallinks">
157                            |
158                        </td>
159                        <td valign="middle" class="ms-globallinks">
160                            <table cellspacing="0" cellpadding="0">
161                                <tr>
162                                    <td class="ms-globallinks">
163                                        <sharepoint:delegatecontrol controlid="GlobalSiteLink1" scope="Farm" runat="server" />
164                                    </td>
165                                    <td class="ms-globallinks">
166                                        <sharepoint:delegatecontrol controlid="GlobalSiteLink2" scope="Farm" runat="server" />
167                                    </td>
168                                </tr>
169                            </table>
170                        </td>
171                        <td valign="middle" class="ms-globallinks">
172                             <a href="javascript:TopHelpButtonClick('NavBarHelpHome')" accesskey="<%$Resources:wss,multipages_helplink_accesskey%>"
173                                id="TopHelpLink" title="<%$Resources:wss,multipages_helplinkalt_text%>" runat="server">
174                                <img id="Img1" align='absmiddle' border="0" src="/_layouts/images/helpicon.gif" alt="<%$Resources:wss,multipages_helplinkalt_text%>"
175                                    runat="server"></a>
176                        </td>
177                    </tr>
178                </table>
179            </td>
180        </tr>
181    </table>
182</asp:Content>
183<asp:Content ID="Content5" ContentPlaceHolderID="PlaceHolderMain" runat="server">
184    <input type="hidden" id="HidOwnerLogin" runat="server"/>
185    <TABLE border="0" cellspacing="0" cellpadding="0" class="ms-propertysheet">
186        <wssuc:InputFormSection Title="<%$Resources:wss,scsignup_titledesc_title%>"
187            Description="<%$Resources:wss,scsignup_titledesc_description%>"
188            runat="server">
189            <Template_InputFormControls>
190                    <wssuc:InputFormControl runat="server"
191                        LabelText="<%$Resources:wss,scsignup_title_label%>"
192                        >
193                        <Template_Control>
194                            <wssawc:InputFormTextBox Title="<%$Resources:wss,scsignup_TxtTitle_Title%>" class="ms-input" ID="TxtTitle" Columns="35" Runat="server" MaxLength=255 />
195                            <wssawc:InputFormRequiredFieldValidator id="ReqValTitle" runat="server"
196                               ErrorMessage="<%$Resources:wss,scsignup_titlefield%>"
197                               ControlToValidate="TxtTitle"/>
198                        </Template_Control>
199                    </wssuc:InputFormControl>
200                    <wssuc:InputFormControl runat="server"
201                        LabelText="<%$Resources:wss,multipages_description%>"
202                        >
203                        <Template_Control>
204                            <wssawc:InputFormTextBox Title="<%$Resources:wss,scsignup_TxtDescription_Title%>" class="ms-input" ID="TxtDescription" Runat="server" TextMode="MultiLine" Columns="40" Rows="3"/>
205                        </Template_Control>
206                    </wssuc:InputFormControl>
207            </Template_InputFormControls>
208        </wssuc:InputFormSection>
209        <wssuc:InputFormSection Title="<%$Resources:wss,scsignup_siteaddress_title%>" runat="server">
210            <Template_Description>
211                <SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,scsignup_siteaddress_desc%>" EncodeMethod='HtmlEncodeAllowSimpleTextFormatting'/>
212                <asp:Label id="LabelURLPrefix" runat="server"/><SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,scsignup_siteaddress_desc2%>" EncodeMethod='HtmlEncodeAllowSimpleTextFormatting'/>
213            </Template_Description>
214            <Template_InputFormControls>
215                    <wssuc:InputFormControl runat="server"
216                        LabelText="<%$Resources:wss,scsignup_sitename_label%>"
217                        >
218                        <Template_Control>
219                            <table cellspacing="0" border="0" cellpadding="0" dir="ltr">
220                                <TR>
221                                    <TD nowrap class="ms-authoringcontrols" style="padding-right:2px">
222                                        <asp:Label id="LabelSiteNamePrefix" runat="server"/>
223                                    </TD>
224                                    <asp:PlaceHolder id="PanelPrefix" runat="server">
225                                        <TD class="ms-authoringcontrols">
226                                            <asp:DropDownList ID="DdlPrefix" Runat="server"></asp:DropDownList>
227                                        </TD>
228                                        <TD class="ms-authoringcontrols" style="padding-left:2px; padding-right:2px">/</TD>
229                                    </asp:PlaceHolder>
230                                    <TD class="ms-authoringcontrols">
231                                        <wssawc:InputFormTextBox Title="<%$Resources:wss,scsignup_TxtSiteName_Title%>" class="ms-input" ID="TxtSiteName" Columns="18" Runat="server" MaxLength=128 />
232                                    </TD>
233                                </TR>
234                            </table>
235                            <wssawc:InputFormRequiredFieldValidator id="ReqValSiteName" runat="server"
236                               BreakBefore=false
237                               ErrorMessage="<%$Resources:wss,scsignup_webfield%>"
238                               ControlToValidate="TxtSiteName"/>
239                            <wssawc:InputFormCustomValidator id="CusValSiteName" runat="server"
240                                ClientValidationFunction="SiteAddressValidate"
241                                ErrorMessage="<%$Resources:wss,scsignup_invalidurl%>"
242                                BreakBefore=false
243                                ControlToValidate="TxtSiteName"/>
244                        </Template_Control>
245                    </wssuc:InputFormControl>
246            </Template_InputFormControls>
247        </wssuc:InputFormSection>
248        <wssuc:TemplatePickerControl id="InputFormTemplatePickerControl" runat="server"
249            ShowSubWebOnly="false" ShowCustomTemplates="false" />
250        <asp:PlaceHolder id="PanelSecondaryContact" runat="server">
251        <wssuc:InputFormSection Title="<%$Resources:wss,scsignup_admins_title%>"
252            Description="<%$Resources:wss,scsignup_admins_desc%>"
253            runat="server">
254            <Template_InputFormControls>
255                <wssuc:InputFormControl runat="server"
256                    LabelText="<%$Resources:wss,scsignup_admins_label%>">
257                    <Template_Control>
258                        <wssawc:PeopleEditor
259                                id="PickerAdmins"
260                                AllowEmpty=false
261                                ValidatorEnabled="true"
262                                runat="server"
263                                SelectionSet="User"
264                                />
265                    </Template_Control>
266                </wssuc:InputFormControl>
267            </Template_InputFormControls>
268        </wssuc:InputFormSection>
269        </asp:PlaceHolder>
270        <SharePoint:DelegateControl runat="server" Id="DelctlCreateSiteCollectionPanel" ControlId="CreateSiteCollectionPanel1" Scope="Farm" />
271        <wssuc:ButtonSection runat="server">
272            <Template_Buttons>
273                <asp:Button UseSubmitBehavior="false" runat="server" class="ms-ButtonHeightWidth" OnClick="BtnCreate_Click" Text="<%$Resources:wss,multipages_createbutton_text%>" id="BtnCreate" accesskey="<%$Resources:wss,multipages_createbutton_accesskey%>"/>
274            </Template_Buttons>
275        </wssuc:ButtonSection>
276    </table>
277</asp:Content>

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:

  1using System;
  2using Lapointe.SharePoint.SiteCollectionProvisioner.CreateSiteWebService;
  3using System.Web.UI.WebControls;
  4using Microsoft.SharePoint.WebControls;
  5using Microsoft.SharePoint.Administration;
  6using Microsoft.SharePoint;
  7using System.Web;
  8using Microsoft.SharePoint.Utilities;
  9using System.Collections.Specialized;
 10using System.Web.UI;
 11
 12namespace Lapointe.SharePoint.SiteCollectionProvisioner.ApplicationPages
 13{
 14    public class CreateSitePage : LayoutsPageBase
 15    {
 16        private const string KEY_CALLED_FROM_OTHER_PRODUCT = "CalledFromOtherProduct";
 17        private const string KEY_DATA_FROM_OTHER_PRODUCT = "Data";
 18        private const string KEY_PORTAL_NAME = "PortalName";
 19        private const string KEY_PORTAL_URL = "PortalUrl";
 20        private const string KEY_PREFIX = "SscPrefix";
 21        private const string KEY_REQUIRE_SECONDARY_CONTACT = "RequireSecondaryContact";
 22        private const string KEY_RETURN_URL = "ReturnUrl";
 23        private const string KEY_TITLE = "Title";
 24        private const string KEY_URLNAME = "UrlName";
 25        private const string KEY_EMAIL = "Email";
 26        private const string KEY_TEMPLATE = "Template";
 27
 28        protected Button BtnCreate;
 29        protected RadioButton CreateDLFalse;
 30        protected RadioButton CreateDLTrue;
 31        protected DropDownList DdlPrefix;
 32        protected DelegateControl DelctlCreateSiteCollectionPanel;
 33        protected TemplatePicker InputFormTemplatePickerControl;
 34        protected Label LabelSiteNamePrefix;
 35        protected Label LabelURLPrefix;
 36        protected PlaceHolder PanelPrefix;
 37        protected PlaceHolder PanelSecondaryContact;
 38        protected PeopleEditor PickerAdmins;
 39        protected InputFormRequiredFieldValidator ReqPickerPeople;
 40        protected TextBox TxtDescription;
 41        protected TextBox TxtDLAlias;
 42        protected TextBox TxtSiteName;
 43        protected TextBox TxtTitle;
 44
 45
 46        #region Properties
 47
 48        /// <summary>
 49        /// Gets the rights required.
 50        /// </summary>
 51        /// <value>The rights required.</value>
 52        protected override SPBasePermissions RightsRequired
 53        {
 54            get
 55            {
 56                return SPBasePermissions.CreateSSCSite;
 57                // Depending on your custom logic it may be necessary to return something other than CreateSSCSite.
 58                //return SPBasePermissions.EmptyMask;
 59            }
 60        }
 61        #endregion
 62
 63        #region Event Handlers
 64
 65        /// <summary>
 66        /// Handles the Click event of the BtnCreate control.
 67        /// </summary>
 68        /// <param name="sender">The source of the event.</param>
 69        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
 70        protected void BtnCreate_Click(object sender, EventArgs e)
 71        {
 72            if (!IsValid)
 73                return;
 74
 75            string managedPath = (string)ViewState[KEY_PREFIX];
 76            if (managedPath == null)
 77            {
 78                managedPath = DdlPrefix.SelectedItem.Value;
 79            }
 80            string siteUrl = managedPath + "/" + TxtSiteName.Text.Trim();
 81
 82            if (siteUrl[0] != '/')
 83            {
 84                siteUrl = "/" + siteUrl;
 85            }
 86            if (siteUrl.Length > 1024)
 87            {
 88                siteUrl = siteUrl.Substring(0, 1024);
 89            }
 90            Uri rootUri = SPAlternateUrl.ContextUri;
 91            SPSite currentSite = SPContext.Current.Site;
 92            SPWeb currentWeb = SPContext.Current.Web;
 93
 94            Uri siteUri = new Uri(rootUri, siteUrl);
 95            siteUrl = siteUri.ToString();
 96
 97            string siteTitle = TxtTitle.Text.Trim();
 98            string siteDescription = TxtDescription.Text.Trim();
 99            uint templateLocaleId = uint.Parse(InputFormTemplatePickerControl.SelectedWebLanguage);
100
101            string ownerLoginName;
102            string ownerName = null;
103            string ownerEmail = currentWeb.CurrentUser.Email;
104            if (currentWeb.CurrentUser.ID != currentSite.SystemAccount.ID)
105            {
106                ownerLoginName = currentWeb.CurrentUser.LoginName;
107                ownerName = currentWeb.CurrentUser.Name;
108            }
109            else
110            {
111                ownerLoginName = Utilities.CurrentUserIdentity.Name;
112            }
113
114            bool hasSecondaryContact = false;
115            string secondaryContactLogin = null;
116            string secondaryContactName = null;
117            string secondaryContactEmail = null;
118            SPUserInfo[] infoArray = null;
119            bool requireSecondaryContact = (bool)ViewState[KEY_REQUIRE_SECONDARY_CONTACT];
120            if (requireSecondaryContact)
121            {
122                // We need to make sure that if a secondary contact is required that the user actually provided one.
123                int count = PickerAdmins.ResolvedEntities.Count;
124                if (count == 0)
125                {
126                    throw new SPException(GetResourceString("scsignup_admins_error", new object[0]));
127                }
128                infoArray = new SPUserInfo[count];
129                count = 0;
130
131                foreach (PickerEntity pickerEntity in PickerAdmins.ResolvedEntities)
132                {
133                    infoArray[count].LoginName = pickerEntity.Key;
134                    infoArray[count].Email = (string)pickerEntity.EntityData[KEY_EMAIL];
135                    infoArray[count].Name = pickerEntity.DisplayText;
136                    infoArray[count].Notes = "";
137
138                    if (!hasSecondaryContact)
139                    {
140                        if (!pickerEntity.Key.Equals(ownerLoginName, StringComparison.CurrentCultureIgnoreCase))
141                        {
142                            hasSecondaryContact = true;
143                            secondaryContactLogin = infoArray[count].LoginName;
144                            secondaryContactEmail = infoArray[count].Email;
145                            secondaryContactName = infoArray[count].Name;
146                        }
147                    }
148                    count++;
149                }
150                if (!hasSecondaryContact)
151                    throw new SPException(GetResourceString("scsignup_admins_error", new object[0]));
152            }
153
154            string portalUrl;
155            string portalName;
156
157            if ((ViewState[KEY_PORTAL_NAME] != null) && (ViewState[KEY_PORTAL_URL] != null))
158            {
159                portalUrl = (string)ViewState[KEY_PORTAL_URL];
160                portalName = (string)ViewState[KEY_PORTAL_NAME];
161            }
162            else
163            {
164                // Comment out the following if you don't want the portal URL and name to be set based on the web application root site.
165                portalUrl = currentSite.PortalUrl;
166                portalName = currentSite.PortalName;
167            }
168
169            string strWebTemplate = "";
170            if (!string.IsNullOrEmpty(ViewState[KEY_TEMPLATE] as string))
171            {
172                strWebTemplate = (string)ViewState[KEY_TEMPLATE];
173            }
174            else if ((InputFormTemplatePickerControl.SelectedWebTemplate != null) && 
175                (InputFormTemplatePickerControl.SelectedWebTemplate.Length <= 127))
176            {
177                strWebTemplate = InputFormTemplatePickerControl.SelectedWebTemplate;
178            }
179
180            bool calledFromOtherProduct = (bool)ViewState[KEY_CALLED_FROM_OTHER_PRODUCT];
181            string returnUrl;
182            if (calledFromOtherProduct)
183            {
184                returnUrl = (string) ViewState[KEY_RETURN_URL];
185                returnUrl = returnUrl + "?Data=" +
186                            HttpUtility.UrlEncode((string) ViewState[KEY_DATA_FROM_OTHER_PRODUCT]) + "&SiteUrl=" +
187                            HttpUtility.UrlEncode(siteUri.ToString());
188            }
189            else
190            {
191                returnUrl = siteUrl;
192            }
193
194            // We have all our data gathered up so now do the actual work...
195            using (SPLongOperation operation = new SPLongOperation(this))
196            {
197                operation.LeadingHTML = "Create Site Collection";
198                operation.TrailingHTML = string.Format("Please wait while the \"{0}\" site collection is being created.", Server.HtmlEncode(siteTitle));
199                operation.Begin();
200
201                // The call to the web service has to run as the process account - otherwise we'd need to grant the
202                // calling user the "Browse User Information" rights to the Central Admin site which we don't want.
203                SPSecurity.RunWithElevatedPrivileges(delegate
204                {
205                    Logger.WriteInformation(string.Format("Calling web service to create site collection \"{0}\"", siteUri.OriginalString));
206                    // We use a Web Service because neither the user nor the process account will have rights to update
207                    // the configuration database (so they can't create the site and we can't even use a timer job so
208                    // our best option is to use the Central Admin site as we know that it's app pool account has the
209                    // rights necessary).
210                    CreateSiteService svc = new CreateSiteService
211                                                {
212                                                    Url =
213                                                        SPAdministrationWebApplication.Local.GetResponseUri(SPUrlZone.Default).ToString().TrimEnd('/') +
214                                                        "/_vti_bin/SCP/CreateSiteService.asmx",
215                                                    Credentials = System.Net.CredentialCache.DefaultCredentials
216                                                };
217                    // We use the managed path as the hint for the database and the quota.  Replace with any other custom logic if needed.
218                    svc.CreateSite(rootUri.ToString(), managedPath, managedPath,
219                                siteUri.OriginalString,
220                                siteTitle, siteDescription, templateLocaleId,
221                                ownerLoginName, ownerName, ownerEmail,
222                                secondaryContactLogin, secondaryContactName,
223                                secondaryContactEmail);
224
225                    try
226                    {
227                        using (SPSite site = new SPSite(siteUrl))
228                        using (SPWeb rootWeb = site.RootWeb)
229                        {
230                            site.AllowUnsafeUpdates = true;
231                            rootWeb.AllowUnsafeUpdates = true;
232
233                            if (requireSecondaryContact)
234                            {
235                                // Add additional users to the site.
236                                rootWeb.SiteUsers.AddCollection(infoArray);
237
238                                foreach (SPUser user in rootWeb.SiteUsers)
239                                {
240                                    if (user.ID == site.SystemAccount.ID)
241                                        continue;
242
243                                    if (!user.IsSiteAdmin)
244                                    {
245                                        user.IsSiteAdmin = true;
246                                        user.Update();
247                                    }
248                                }
249                            }
250                            // Create the default Members, Owners, and Visitors groups.
251                            rootWeb.CreateDefaultAssociatedGroups(ownerLoginName, secondaryContactLogin, string.Empty);
252
253                            // Save the site directory data.
254                            SaveSiteDirectoryData(DelctlCreateSiteCollectionPanel, site);
255
256                            // Link to the main portal
257                            site.PortalUrl = portalUrl;
258                            site.PortalName = portalName;
259
260                            // Apply the selected web template
261                            rootWeb.ApplyWebTemplate(strWebTemplate);
262
263                            //TODO: add any additional custom logic to activate features based on user provided data.
264                        }
265                    }
266                    catch (Exception ex)
267                    {
268                        Logger.WriteException(ex, string.Format("Failed to update site collection \"{0}\" (the site collection was created).", siteUrl));
269                        throw;
270                    }
271                    Logger.WriteSuccessAudit(string.Format("Successfully created and updated site collection \"{0}\"", siteUrl));
272
273                });
274
275                operation.End(returnUrl, SPRedirectFlags.Static, Context, null);
276            }
277        }
278
279        /// <summary>
280        /// Raises the <see cref="E:Load"/> event.
281        /// </summary>
282        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
283        protected override void OnLoad(EventArgs e)
284        {
285            if (!Utilities.CurrentUserIdentity.IsAuthenticated)
286            {
287                // Don't allow anonymous access.
288                SPUtility.SendAccessDeniedHeader(new UnauthorizedAccessException());
289            }
290            if (!SPUtility.OriginalServerRelativeRequestUrl.StartsWith("/_layouts/"))
291            {
292                // Make sure we're running from the root site collection's _layouts folder.
293                Utilities.SendResponse(Response, 403, "403 FORBIDDEN");
294            }
295
296            base.OnLoad(e);
297
298            SPWebApplication webApplication = SPContext.Current.Site.WebApplication;
299
300            /*************************************************************************************/
301            /** Comment out the following if you wish to NOT enable self service site creation. **/
302            if (!webApplication.SelfServiceSiteCreationEnabled)
303            {
304                throw new SPException(SPResource.GetString("SscIsNotEnabled", new object[0]));
305            }
306            /*************************************************************************************/
307
308
309            /*************************************************************************************/
310            /** Uncomment the following if you wish to require the user belong to a specific    **/
311            /** SharePoint Group in the current site (or replace with other custom logic).      **/
312            //if (!SPContext.Current.Web.SiteGroups["GROUPNAME"].ContainsCurrentUser)
313            //    SPUtility.SendAccessDeniedHeader(new UnauthorizedAccessException());
314            /*************************************************************************************/
315
316            SPAlternateUrl responseUrl = webApplication.AlternateUrls.GetResponseUrl(SPUrlZone.Default);
317            bool requireSecondaryContact = webApplication.RequireContactForSelfServiceSiteCreation;
318
319            if (SPContext.Current.Site.HostHeaderIsSiteName)
320            {
321                if (responseUrl != null)
322                {
323                    throw new SPException(
324                        SPResource.GetString("SscNotAvailableOnHostHeaderSite", new object[] { responseUrl.IncomingUrl }));
325                }
326                throw new SPException(SPResource.GetString("SscIsNotEnabled", new object[0]));
327            }
328
329            if (!Page.IsPostBack)
330            {
331                // Store any passed in variables for use during postback.
332                ViewState[KEY_REQUIRE_SECONDARY_CONTACT] = requireSecondaryContact;
333
334                if ((Request[KEY_DATA_FROM_OTHER_PRODUCT] == null) || (Request[KEY_RETURN_URL] == null))
335                {
336                    ViewState[KEY_CALLED_FROM_OTHER_PRODUCT] = false;
337                }
338                else
339                {
340                    ViewState[KEY_CALLED_FROM_OTHER_PRODUCT] = true;
341                    ViewState[KEY_DATA_FROM_OTHER_PRODUCT] = Request[KEY_DATA_FROM_OTHER_PRODUCT];
342                    ViewState[KEY_RETURN_URL] = Request[KEY_RETURN_URL];
343                }
344
345                if (!string.IsNullOrEmpty(Request[KEY_PORTAL_NAME]))
346                {
347                    ViewState[KEY_PORTAL_NAME] = Request[KEY_PORTAL_NAME];
348                }
349
350                if (!string.IsNullOrEmpty(Request[KEY_PORTAL_URL]))
351                {
352                    ViewState[KEY_PORTAL_URL] = Request[KEY_PORTAL_URL];
353                }
354
355                if (!string.IsNullOrEmpty(Request[KEY_URLNAME]))
356                {
357                    TxtSiteName.Text = Request.QueryString[KEY_URLNAME].Trim();
358                    //TxtSiteName.ReadOnly = true;
359                }
360
361                if (!string.IsNullOrEmpty(Request[KEY_TITLE]))
362                {
363                    TxtTitle.Text = Request.QueryString[KEY_TITLE].Trim();
364                    //TxtTitle.ReadOnly = true;
365                }
366
367                if (!string.IsNullOrEmpty(Request[KEY_TEMPLATE]))
368                {
369                    ViewState[KEY_TEMPLATE] = Request[KEY_TEMPLATE];
370                    InputFormTemplatePickerControl.Visible = false;
371                }
372
373
374                string passedInPrefixName = Request[KEY_PREFIX];
375                string defaultPrefixName = null;
376                StringCollection wildcardPrefixNames = new StringCollection();
377
378
379                foreach (SPPrefix prefix in webApplication.Prefixes)
380                {
381                    if (prefix.PrefixType != SPPrefixType.WildcardInclusion)
382                        continue;
383
384                    wildcardPrefixNames.Add(prefix.Name);
385                    defaultPrefixName = prefix.Name;
386                }
387                if (wildcardPrefixNames.Count == 0)
388                {
389                    throw new SPException(SPResource.GetString("NoInclusionDefinedForSsc", new object[0]));
390                }
391                if (wildcardPrefixNames.Count == 1)
392                {
393                    ViewState[KEY_PREFIX] = defaultPrefixName;
394                    PanelPrefix.Visible = false;
395                    LabelSiteNamePrefix.Text = SPHttpUtility.HtmlEncode(responseUrl.IncomingUrl.TrimEnd('/') + "/" + defaultPrefixName + "/");
396                    LabelURLPrefix.Text = LabelSiteNamePrefix.Text;
397                }
398                else
399                {
400                    foreach (string prefixName in wildcardPrefixNames)
401                    {
402                        if (prefixName.Length != 0)
403                        {
404                            DdlPrefix.Items.Add(new ListItem(prefixName, prefixName));
405                        }
406                        else
407                            DdlPrefix.Items.Add(new ListItem(SPResource.GetString("RootNone", new object[0]), prefixName));
408                    }
409                    LabelSiteNamePrefix.Text = SPHttpUtility.HtmlEncode(responseUrl.IncomingUrl.TrimEnd('/') + "/");
410                    LabelURLPrefix.Text = LabelSiteNamePrefix.Text + wildcardPrefixNames[0] + "/";
411                }
412                if (!string.IsNullOrEmpty(passedInPrefixName) && wildcardPrefixNames.Contains(passedInPrefixName))
413                {
414                    ViewState[KEY_PREFIX] = passedInPrefixName;
415                    PanelPrefix.Visible = false;
416                    LabelSiteNamePrefix.Text = SPHttpUtility.HtmlEncode(responseUrl.IncomingUrl.TrimEnd('/') + "/" + passedInPrefixName + "/");
417                    LabelURLPrefix.Text = LabelSiteNamePrefix.Text;
418                }
419
420                if (!requireSecondaryContact)
421                {
422                    PanelSecondaryContact.Visible = false;
423                }
424            
425            }
426            if (requireSecondaryContact)
427            {
428                SPPrincipalSource principalSource = PickerAdmins.PrincipalSource;
429                PickerAdmins.PrincipalSource = principalSource & ~SPPrincipalSource.UserInfoList;
430            }
431        }
432
433        #endregion
434
435        #region Helper Methods
436
437        /// <summary>
438        /// Saves the site directory data.
439        /// </summary>
440        /// <param name="delegateControl">The delegate control.</param>
441        /// <param name="site">The site.</param>
442        internal void SaveSiteDirectoryData(DelegateControl delegateControl, SPSite site)
443        {
444            if (site == null)
445                return;
446
447            foreach (Control control in delegateControl.Controls)
448            {
449                IFormDelegateControlSource source = control as IFormDelegateControlSource;
450                if (source == null)
451                    continue;
452
453                source.OnFormSave(site);
454            }
455        }
456
457        #endregion
458
459    }
460
461}

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:

 1using System;
 2using Microsoft.SharePoint;
 3using Microsoft.SharePoint.WebControls;
 4using System.Net;
 5using System.Web.Services;
 6
 7namespace Lapointe.SharePoint.SiteCollectionProvisioner.WebServices
 8{
 9    [WebService(Namespace = "http://schemas.falchionconsulting.com/sharepoint/soap/")]
10    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
11    public class CreateSiteService : WebService
12    {
13        public CreateSiteService() {}
14
15        [WebMethod]
16        public void CreateSite(string webAppUrl,
17            string databaseHint,
18            string quotaHint,
19            string siteUrl,
20            string title,
21            string description,
22            uint templateLocaleId,
23            string ownerLogin,
24            string ownerName,
25            string ownerEmail,
26            string secondaryContactLogin,
27            string secondaryContactName,
28            string secondaryContactEmail)
29        {
30            try
31            {
32                SPSecurity.RunWithElevatedPrivileges(
33                    delegate
34                    {
35                        Utilities.CreateSite(new Uri(webAppUrl), databaseHint, quotaHint, siteUrl,
36                                             title, description, templateLocaleId,
37                                             ownerLogin, ownerName, ownerEmail,
38                                             secondaryContactLogin,
39                                             secondaryContactName,
40                                             secondaryContactEmail);
41                    });
42            }
43            catch (Exception ex)
44            {
45
46                Logger.WriteException(ex);
47            }
48
49        }
50    }
51}

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:

  1using System;
  2using Microsoft.SharePoint.Administration;
  3using System.Security.Permissions;
  4using System.Security.Principal;
  5using System.Web;
  6using Microsoft.SharePoint;
  7
  8namespace Lapointe.SharePoint.SiteCollectionProvisioner
  9{
 10    public class Utilities
 11    {
 12
 13
 14        /// <summary>
 15        /// Gets the current user identity.
 16        /// </summary>
 17        /// <value>The current user identity.</value>
 18        public static IIdentity CurrentUserIdentity
 19        {
 20            [SecurityPermission(SecurityAction.Assert, Flags = SecurityPermissionFlag.ControlPrincipal)]
 21            get
 22            {
 23                if (HttpContext.Current != null)
 24                {
 25                    if (HttpContext.Current.User == null)
 26                    {
 27                        return new GenericIdentity(string.Empty);
 28                    }
 29                    return HttpContext.Current.User.Identity;
 30                }
 31                return WindowsIdentity.GetCurrent();
 32            }
 33        }
 34
 35        /// <summary>
 36        /// Sends an HTTP response code to the browser.
 37        /// </summary>
 38        /// <param name="response">The HTTP Response object.</param>
 39        /// <param name="code">The response code to send.</param>
 40        /// <param name="body">The body text to send.</param>
 41        public static void SendResponse(HttpResponse response, int code, string body)
 42        {
 43            HttpContext current = HttpContext.Current;
 44            bool? responseEnded = current.Items["ResponseEnded"] as bool?;
 45            if (!responseEnded.HasValue || !responseEnded.Value)
 46            {
 47                current.Items["ResponseEnded"] = true;
 48                response.StatusCode = code;
 49                response.Clear();
 50                if (body != null)
 51                {
 52                    response.Write(body);
 53                }
 54                response.End();
 55            }
 56        }
 57
 58
 59        /// <summary>
 60        /// Creates the site collection.
 61        /// </summary>
 62        /// <param name="webAppUri">The web app URI.</param>
 63        /// <param name="databaseHint">The database hint.</param>
 64        /// <param name="quotaHint">The quota hint.</param>
 65        /// <param name="siteUrl">The site URL.</param>
 66        /// <param name="title">The title.</param>
 67        /// <param name="description">The description.</param>
 68        /// <param name="nLCID">The n LCID.</param>
 69        /// <param name="ownerLogin">The owner login.</param>
 70        /// <param name="ownerName">Name of the owner.</param>
 71        /// <param name="ownerEmail">The owner email.</param>
 72        /// <param name="contactLogin">The contact login.</param>
 73        /// <param name="contactName">Name of the contact.</param>
 74        /// <param name="contactEmail">The contact email.</param>
 75        public static void CreateSite(Uri webAppUri, string databaseHint, string quotaHint, string siteUrl, string title, string description, uint nLCID, string ownerLogin, string ownerName, string ownerEmail, string contactLogin, string contactName, string contactEmail)
 76        {
 77            Logger.WriteInformation(string.Format("Creating site collection \"{0}\".", siteUrl));
 78
 79            SPWebApplication webApp = SPWebApplication.Lookup(webAppUri);
 80
 81            SPContentDatabase targetDb = GetTargetDatabase(webApp, databaseHint);
 82            SPQuotaTemplate quota = GetQuotaTemplate(webApp, quotaHint);
 83
 84            SPSite site = null;
 85            try
 86            {
 87                if (targetDb == null)
 88                {
 89                    // We don't have a specific database so just let SP figure out where to put it.
 90                    site = webApp.Sites.Add(siteUrl, title, description, nLCID, null, ownerLogin, ownerName, ownerEmail,
 91                                            contactLogin, contactName, contactEmail, false);
 92                }
 93                else
 94                {
 95                    if (targetDb.CurrentSiteCount == targetDb.MaximumSiteCount)
 96                        throw new SPException(string.Format("The database {0} has reached its maximum site count and cannot be added to.", targetDb.Name));
 97
 98                    site = targetDb.Sites.Add(siteUrl, title, description, nLCID, null, ownerLogin, ownerName,
 99                                              ownerEmail, contactLogin, contactName, contactEmail, false);
100                    
101                }
102
103                if (quota != null)
104                {
105                    site.Quota = quota;
106                }
107            }
108            catch (Exception ex)
109            {
110                Logger.WriteException(ex, string.Format("Failed to create site collection \"{0}\".", siteUrl));
111                throw;
112            }
113            finally
114            {
115                if (site != null)
116                    site.Dispose();
117            }
118            Logger.WriteSuccessAudit(string.Format("Successfully created site collection \"{0}\"", siteUrl));
119        }
120
121        /// <summary>
122        /// Gets the target database that matches the specified prefix name within the given web application.
123        /// </summary>
124        /// <param name="webApp">The web app.</param>
125        /// <param name="databaseHint">Name of the prefix.</param>
126        /// <returns></returns>
127        public static SPContentDatabase GetTargetDatabase(SPWebApplication webApp, string databaseHint)
128        {
129            SPContentDatabase targetDb = default(SPContentDatabase);
130
131            // If a new managed path is added it will be necessary to either add a corresponding content
132            // database to the web application (must contain the managed path name in the content db name)
133            // or alternatively you must add code as shown in the comments below to force the use of an
134            // existing content database (it is recommended to add a new content database for every managed path
135            // rather than the approach below).
136            //if (databaseHint.ToLower() == "path2")
137            //   databaseHint = "path1";
138
139            foreach (SPContentDatabase db in webApp.ContentDatabases)
140            {
141                if (db.Name.ToLower().Contains(databaseHint.ToLower()) && db.MaximumSiteCount > db.CurrentSiteCount)
142                {
143                    targetDb = db;
144                    break;
145                }
146            }
147            
148            return targetDb;
149        }
150
151        /// <summary>
152        /// Gets the quota template that matches the specified prefix name.
153        /// </summary>
154        /// <param name="webApp">The web app.</param>
155        /// <param name="quotaHint">Name of the prefix.</param>
156        /// <returns></returns>
157        public static SPQuotaTemplate GetQuotaTemplate(SPWebApplication webApp, string quotaHint)
158        {
159            // If a new managed path is added it will be necessary to either add a corresponding quota
160            // template to (must contain the managed path name in the quota template name)
161            // or alternatively you must add code as shown in the comments below to force the use of an
162            // existing quota template (it is recommended to add a new quota template for every managed path
163            // rather than the approach below).
164            //if (quotaHint.ToLower() == "path2")
165            //   quotaHint = "path1";
166
167            SPQuotaTemplate quota = default(SPQuotaTemplate);
168            SPQuotaTemplateCollection quotaColl = SPFarm.Local.Services.GetValue<SPWebService>("").QuotaTemplates;
169
170            foreach (SPQuotaTemplate q in quotaColl)
171            {
172                if (q.Name.ToLower().Contains(quotaHint.ToLower()))
173                {
174                    quota = q;
175                    break;
176                }
177            }
178
179            if (quota == default(SPQuotaTemplate))
180                quota = quotaColl[webApp.DefaultQuotaTemplate];
181
182            return quota;
183        }
184
185    }
186}

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 NameDescriptionExample Usage
DataAllows 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./_Layouts/SCP/CreateSite.aspx?Data=480E13C2-DCA5-4a76-ACE1-10A82F7181B2
ReturnUrlThe URL to return to after creating the site collection./_Layouts/SCP/CreateSite.aspx?ReturnUrl=http%3A%2F%2Fportal%2Fdepartments%2FHR
PortalNameThe name of the portal site to link back to. This sets the `SPWeb.PortalName `property./_Layouts/SCP/CreateSite.aspx?PortalName=Main%20Portal
PortalUrlThe URL of the portal site to link back to. This sets the `SPWeb.PortalUrl `property./_Layouts/SCP/CreateSite.aspx?PortalUrl=http%3A%2F%2Fportal%2F
UrlNameThe name to use for the site collection URL./_Layouts/SCP/CreateSite.aspx?UrlName=hrteamsite
TitleThe title of the site collection to create./_Layouts/SCP/CreateSite.aspx?Title=HR%20Team%20Site
SscPrefixThe managed path to create the site under. Specifying this value will prevent the user from choosing a different value./_Layouts/SCP/CreateSite.aspx?SscPrefix=departments
TemplateThe site template to use when creating the site collection. Specifying this parameter will hide the site template picker from the form./_Layouts/SCP/CreateSite.aspx?Template=STS%230
SiteDirectoryThe site directory used to store an entry for the new site collection./_Layouts/SCP/CreateSite.aspx?SiteDirectory=http%3A%2F%2Fportal%2FSiteDirectory&EnforceListing=False&EntryReq=1
EnforceListingIndicates whether an entry in the site directory is required. Valid values are "True" or "False".(see above)
EntryReqThe 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)

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 :).