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:
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 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. | /_Layouts/SCP/CreateSite.aspx?Data=480E13C2-DCA5-4a76-ACE1-10A82F7181B2 |
ReturnUrl | The URL to return to after creating the site collection. | /_Layouts/SCP/CreateSite.aspx?ReturnUrl=http%3A%2F%2Fportal%2Fdepartments%2FHR |
PortalName | The name of the portal site to link back to. This sets the `SPWeb.PortalName `property. | /_Layouts/SCP/CreateSite.aspx?PortalName=Main%20Portal |
PortalUrl | The 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 |
UrlName | The name to use for the site collection URL. | /_Layouts/SCP/CreateSite.aspx?UrlName=hrteamsite |
Title | The title of the site collection to create. | /_Layouts/SCP/CreateSite.aspx?Title=HR%20Team%20Site |
SscPrefix | The 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 |
Template | The 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 |
SiteDirectory | The 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 |
EnforceListing | 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) |
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 :).