SharePoint Automation Gary Lapointe – Founding Partner, Aptillon, Inc.

17Apr/111

European SharePoint Best Practices Conference Wrap Up

I just got back from London and all I can say is, “Wow!” This was the first time that myself and my wife and daughter have ever been out of the US and we had an absolute blast – we did so much walking that we literally wore our shoes thin – it’s truly an amazing place with incredible history everywhere you turn.

As for the conference itself, first off I want to thank Steve Smith and all those that were involved with organizing such a wonderful conference – as a speaker they definitely set the bar for other conference organizers and I hope that attendees saw the same attention to detail and overall quality that the speakers saw (this conference is truly unique amongst all the ones I’ve spoken at).

In regards to my two sessions – I totally blew my timing on both of them (more so on the developer one) and was unable to show everything that I planned but I do hope that those that attended got some useful information for the time spent (virtually nobody left when I ran late so I’m going to take that as a good sign that people were getting value out of the presentations). I plan to post some of the sample scripts that I demonstrated during the presentations but that will come over the next few weeks; for now I’ve posted my slide decks (saved with notes so you can see some of my examples) for you to download:

Windows PowerShell for SharePoint 2010 Administrators Windows PowerShell for SharePoint 2010 Administrators
Windows PowerShell for SharePoint 2010 Developers Windows PowerShell for SharePoint 2010 Developers

Don’t forget that you can also download the PowerShell cheat sheet that I provided during the sessions (see my earlier post).

Thanks again to everyone that attended my sessions and for all the great tweets that came out during and after the sessions – I know I still have lots to learn when it comes to public speaking so any kind of feedback is very much appreciated!

14May/1016

Announcing My SharePoint 2010 PowerShell Cmdlets & STSADM Commands Now Available for Download

I’ve been wanting to release the SharePoint 2010 version of my STSADM extensions for quite some time but honestly just haven’t had the time to migrate as many as I would have liked. With over 145 STSADM extensions for SharePoint 2007 it was a challenge determining which ones I should focus on initially for the migration.

But today I’m happy to announce my initial release which contains 46 PowerShell cmdlets and 56 STSADM commands specific to SharePoint 2010. Yup, you read right, I’ve decided to maintain support for my STSADM commands and have been migrating them over as I create the equivalent replacement PowerShell cmdlet (though I recommend you don’t use them and suck it up and get used to PowerShell). You should note that there are more STSADM commands than PowerShell cmdlets – that’s because some of the things I was doing with STSADM can now easily be done with out of the box PowerShell cmdlets (I also have new PowerShell cmdlets that do not have an STSADM equivalent – everything new I create will be a cmdlet and I’ll create no new STSADM commands).

It’s going to take me a while to create all the posts needed to explain each cmdlet (assuming I create one at all) so for now I’ve created this simple table which lists all the STSADM commands and PowerShell cmdlets that are available in this initial release (I’ll eventually update my command index page but for now let this serve as the main reference for what is available as of 5/14/2010):

STSADM Commands

PowerShell Cmdlets

Notes

gl-activatefeature

Enable-SPFeature2

There’s an OOTB Enable-SPFeature cmdlet, this one simple adds some capabilities which were present in my existing STSADM command.

gl-addaudiencerule

New-SPAudienceRule

 

gl-addavailablesitetemplate

  I’ll eventually create a cmdlet for this.

gl-adduser2

  Use the OOTB New-SPUser cmdlet.

gl-adduserpolicyforwebapp

Add-SPWebApplicationUserPolicy

 

gl-applytheme

  This can be done pretty easily using Get-SPWeb and the ApplyTheme() method of the SPWeb object.

gl-backup

  Use the OOTB Backup-SPFarm and Backup-SPSite cmdlets.

gl-backupsites

Backup-SPSite2

Extends Backup-SPSite by including IIS settings.

gl-convertsubsitetositecollection

ConvertTo-SPSite

 

gl-copycontenttypes

Copy-SPContentType

 

gl-copylist

Copy-SPList

 

gl-copylistsecurity

Copy-SPListSecurity

 

gl-createaudience

New-SPAudience

 

gl-createcontentdb

  Use the OOTB New-SPContentDatabase cmdlet.

gl-createpublishingpage

New-SPPublishingPage

 

gl-createquotatemplate

New-SPQuotaTemplate

 

gl-createwebapp

  Use the OOTB New-SPWebApplication cmdlet

gl-deactivatefeature

Disable-SPFeature2

Extends the OOTB Disable-SPFeature cmdlet.

gl-deleteallusers

  I probably won’t replicate this as it is easily done using the OOTB Remove-SPUser cmdlet.

gl-deleteaudience

Remove-SPAudience

 

gl-deletelist

Remove-SPList

 

gl-deletewebapp

  Use the OOTB Remove-SPWebApplication cmdlet.

gl-disableuserpermissionforwebapp

  This is fairly easy to do OOTB so I may not create a cmdlet for it.

gl-editquotatemplate

Set-SPQuotaTemplate

 

gl-enableuserpermissionforwebapp

  This is fairly easy to do OOTB so I may not create a cmdlet for it.

gl-enumaudiencerules

Export-SPAudienceRules

 

gl-enumavailablepagelayouts

Get-SPPublishingPageLayout

 

gl-enumavailablesitetemplates

Get-SPAvailableWebTemplates

 

gl-enumeffectivebaseperms

  This is fairly easy to do OOTB so I may not create a cmdlet for it.

gl-enumfeatures

  Use the OOTB Get-SPFeature cmdlet.

gl-enuminstalledsitetemplates

  Use the OOTB Get-SPWebTemplate cmdlet.

gl-enumpagewebparts

Get-SPWebPartList

 

gl-enumunghostedfiles

Get-SPCustomizedPages

 

gl-execadmsvcjobs

Start-SPAdminJob2

I honestly need to research this a bit more as I’m not sure it’s necessary anymore but I’ve replicated the functionality in case someone finds it useful.

gl-exportaudiences

Export-SPAudiences

 

gl-exportcontenttypes

Export-SPContentType

 

gl-exportlist

Export-SPWeb2

I’ve just extended the Export-SPWeb2 cmdlet to add additional parameters.

gl-exportlistsecurity

Export-SPListSecurity

 

gl-extendwebapp

  Use the OOTB New-SPWebApplicationExtension cmdlet.

gl-fixpublishingpagespagelayouturl

Repair-SPPageLayoutUrl

 

gl-importaudiences

Import-SPAudiences

 

gl-importlist

Import-SPWeb2

I’ve just extended the Import-SPWeb2 cmdlet to add additional parameters.

gl-importlistsecurity

Import-SPListSecurity

 

gl-listaudiencetargeting

Set-SPListAudienceTargeting

 

gl-managecontentdbsettings

  Use the OOTB Set-SPContentDatabase cmdlet.

gl-propagatecontenttype

Propagate-SPContentType

 

gl-publishitems

Publish-SPListItems

 

gl-reghostfile

Reset-SPCustomizedPages

 

gl-removeavailablesitetemplate

  I’ll eventually create a cmdlet for this (maybe).

gl-repairsitecollectionimportedfromsubsite

Repair-SPSite

 

gl-replacewebpartcontent

Replace-SPWebPartContent

 

gl-setbackconnectionhostnames

Set-SPBackConnectionHostNames

 

gl-setselfservicesitecreation

  Not sure if I’ll migrate this or not.

gl-syncquotas

Set-SPQuota

 

gl-tracelog

  Use the OOTB Set-SPDiagnosticConfig cmdlet.

gl-unextendwebapp

  Use the OOTB Remove-SPWebApplication cmdlet.
 

Get-SPAudience

 
 

Get-SPAudienceManager

 
 

Get-SPContentType

 
 

Get-SPFile

 
 

Get-SPLimitedWebPartManager

 
 

Get-SPList

 
 

Get-SPPublishingPage

 
 

Get-SPQuotaTemplate

 
 

Set-SPAudience

 

For those that know a thing or two about cmdlet development you might be interested in knowing that I am dynamically generating the help XML file for the cmdlets. If you download the source you’ll find a class which uses reflection to interrogate the assembly and dynamically build the help file just prior to building the WSP package. This saved me literally days of hand editing XML.

You can download the source and WSP files here or from the Downloads page:

After you deploy the package you can type “help <cmdlet name>” to get detailed help about each cmdlet, including parameter descriptions and example usage. If you want to see the list of cmdlets installed type the following:

gcm | where {$_.DLL –like "*lapointe*"}

As always, your use of these cmdlets/stsadm commands is at your own risk – I do as much testing as I can but every environment is different and there’s simply not enough time in a day. If you have any suggestions or feedback please don’t hesitate to leave a comment – I appreciate all of them!

9Jul/0918

Custom SharePoint 2007 Site Collection Creation Page

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:

<%@ Assembly Name="Microsoft.SharePoint.ApplicationPages, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>
<%@ 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"%>
<%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Register TagPrefix="wssuc" TagName="InputFormSection" src="~/_controltemplates/InputFormSection.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="InputFormControl" src="~/_controltemplates/InputFormControl.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="ButtonSection" src="~/_controltemplates/ButtonSection.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="TemplatePickerControl" src="~/_controltemplates/TemplatePickerControl.ascx" %>
<%@ Register Tagprefix="wssawc" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="wssuc" TagName="Welcome" src="~/_controltemplates/Welcome.ascx" %>
<asp:Content ID="Content1" contentplaceholderid="PlaceHolderPageTitle" runat="server">
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,scsignup_pagetitle%>" EncodeMethod='HtmlEncode'/>
</asp:Content>
<asp:Content ID="Content2" contentplaceholderid="PlaceHolderPageTitleInTitleArea" runat="server">
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,scsignup_pagetitle%>" EncodeMethod='HtmlEncode'/>
</asp:Content>
<asp:Content ID="Content3" contentplaceholderid="PlaceHolderPageDescription" runat="server">
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,scsignup_pagedescription%>" EncodeMethod='HtmlEncode'/>
</asp:Content>
<asp:Content ID="Content4" contentplaceholderid="PlaceHolderAdditionalPageHead" runat="server">
<script src="/_layouts/<%=System.Threading.Thread.CurrentThread.CurrentUICulture.LCID%>/commonvalidation.js"></script>

<script Language="javascript">

function    Visascii(ch)
{
    return (!(ch.charCodeAt(0) & 0x80));
}
function Visspace(ch)
{
    return (ch.charCodeAt(0) == 32) || ((9 <= ch.charCodeAt(0)) && (ch.charCodeAt(0) <= 13));
}
function stripWS(str)
{
    var b = 0;
    var e = str.length;
    while (str.charAt(b) && (Visascii(str.charAt(b)) && Visspace(str.charAt(b))))
        b++;
    while ((b < e) && (Visascii(str.charAt(e-1)) && Visspace(str.charAt(e-1))))
        e--;
    return ((b>=e)?"":str.substring(b, e ));
}
var L_NoFieldEmpty_TEXT = "<SharePoint:EncodedLiteral runat='server' text='<%$Resources:wss,common_nofieldempty_TEXT%>' EncodeMethod='EcmaScriptStringLiteralEncode'/>";
function CheckForEmptyField(text_orig,field_name)
{
    var text = stripWS(text_orig);
    if (text.length == 0)
    {
        alert(StBuildParam(L_NoFieldEmpty_TEXT, field_name));
        return false;
    }
    return (true);
}
function CheckForEmptyFieldNoAlert(text_orig)
{
    var text = stripWS(text_orig);
    if (text.length == 0)
    {
        return false;
    }
    return (true);
}
var L_WrongEmailName_TEXT = "<SharePoint:EncodedLiteral runat='server' text='<%$Resources:wss,common_wrongemailname_TEXT%>' EncodeMethod='EcmaScriptStringLiteralEncode'/>";
function CheckForAtSighInEmailName(text_orig,field_name)
{
    var text = stripWS(text_orig);
    if (!CheckForEmptyField(text_orig,field_name)) return false;
    var indexAt = 0;
    var countAt = 0;
    var countSpace = 0;
    var len = text.length;
    while(len--)
    {
        if (text.charAt(len) == '@')
        {
            indexAt = len;
            countAt++;
        }
        if (text.charAt(len) == ' ')
            countSpace ++;
    }
    if ((countAt == 0) ||
        (indexAt == 0) ||
        (indexAt == (text.length-1))
        )
    {
        alert(StBuildParam(L_WrongEmailName_TEXT, field_name));
        return false;
    }
    if (countSpace !=0 )
    {
        alert(L_TextWithoutSpaces1_TEXT + field_name);
        return false;
    }
    return (true);
}
    function _spBodyOnLoad()
    {
        try{document.getElementById(<%SPHttpUtility.AddQuote(SPHttpUtility.NoEncode(TxtTitle.ClientID),Response.Output);%>).focus();}catch(e){}
    }
    function SiteAddressValidate(source, args)
    {
        var stname = stripWS(args.Value);
        if(IndexOfIllegalCharInUrlLeafName(stname) != -1)
        {
            args.IsValid = false;
            return;
        }
        args.IsValid = true;
    }

</script>

</asp:Content>
<asp:Content contentplaceholderid="PlaceHolderTitleBreadcrumb" runat="server"><br /></asp:Content>
<asp:Content contentplaceholderid="PlaceHolderFormDigest" runat="server">
<SharePoint:FormDigest runat=server/>
</asp:Content>
<asp:Content ContentPlaceHolderID="PlaceHolderGlobalNavigation" runat="server">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td colspan="4" class="ms-globalbreadcrumb">
<span id="TurnOnAccessibility" style="display: none"><a href="#" class="ms-skip"
onclick="SetIsAccessibilityFeatureEnabled(true);UpdateAccessibilityUI();return false;">
<sharepoint:encodedliteral runat="server" text="<%$Resources:wss,master_turnonaccessibility%>"
encodemethod="HtmlEncode" />
</a></span><a id="A1" href="javascript:;" onclick="javascript:this.href='#mainContent';"
class="ms-skip" accesskey="<%$Resources:wss,maincontent_accesskey%>" runat="server">
<sharepoint:encodedliteral runat="server" text="<%$Resources:wss,mainContentLink%>"
encodemethod="HtmlEncode" />
</a>
<table cellpadding="0" cellspacing="0" height="100%" class="ms-globalleft">
<tr>
<td class="ms-globallinks" style="padding-top: 2px;" height="100%" valign="middle">
<div>
<span id="TurnOffAccessibility" style="display: none"><a href="#" class="ms-acclink"
onclick="SetIsAccessibilityFeatureEnabled(false);UpdateAccessibilityUI();return false;">
<sharepoint:encodedliteral runat="server" text="<%$Resources:wss,master_turnoffaccessibility%>"
encodemethod="HtmlEncode" />
</a></span>
</div>
</td>
</tr>
</table>
<table cellpadding="0" cellspacing="0" height="100%" class="ms-globalright">
<tr>
<td valign="middle" class="ms-globallinks" style="padding-left: 3px; padding-right: 6px;">
</td>
<td valign="middle" class="ms-globallinks">
<wssuc:Welcome id="IdWelcome" runat="server" EnableViewState="false">
</wssuc:Welcome>
</td>
<td style="padding-left: 1px; padding-right: 3px;" class="ms-globallinks">
|
</td>
<td valign="middle" class="ms-globallinks">
<table cellspacing="0" cellpadding="0">
<tr>
<td class="ms-globallinks">
<sharepoint:delegatecontrol controlid="GlobalSiteLink1" scope="Farm" runat="server" />
</td>
<td class="ms-globallinks">
<sharepoint:delegatecontrol controlid="GlobalSiteLink2" scope="Farm" runat="server" />
</td>
</tr>
</table>
</td>
<td valign="middle" class="ms-globallinks">
&nbsp; <a href="javascript:TopHelpButtonClick('NavBarHelpHome')" accesskey="<%$Resources:wss,multipages_helplink_accesskey%>"
id="TopHelpLink" title="<%$Resources:wss,multipages_helplinkalt_text%>" runat="server">
<img id="Img1" align='absmiddle' border="0" src="/_layouts/images/helpicon.gif" alt="<%$Resources:wss,multipages_helplinkalt_text%>"
runat="server"></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderID="PlaceHolderMain" runat="server">
<input type="hidden" id="HidOwnerLogin" runat="server"/>
<TABLE border="0" cellspacing="0" cellpadding="0" class="ms-propertysheet">
<wssuc:InputFormSection Title="<%$Resources:wss,scsignup_titledesc_title%>"
Description="<%$Resources:wss,scsignup_titledesc_description%>"
runat="server">
<Template_InputFormControls>
<wssuc:InputFormControl runat="server"
LabelText="<%$Resources:wss,scsignup_title_label%>"
>
<Template_Control>
<wssawc:InputFormTextBox Title="<%$Resources:wss,scsignup_TxtTitle_Title%>" class="ms-input" ID="TxtTitle" Columns="35" Runat="server" MaxLength=255 />
<wssawc:InputFormRequiredFieldValidator id="ReqValTitle" runat="server"
ErrorMessage="<%$Resources:wss,scsignup_titlefield%>"
ControlToValidate="TxtTitle"/>
</Template_Control>
</wssuc:InputFormControl>
<wssuc:InputFormControl runat="server"
LabelText="<%$Resources:wss,multipages_description%>"
>
<Template_Control>
<wssawc:InputFormTextBox Title="<%$Resources:wss,scsignup_TxtDescription_Title%>" class="ms-input" ID="TxtDescription" Runat="server" TextMode="MultiLine" Columns="40" Rows="3"/>
</Template_Control>
</wssuc:InputFormControl>
</Template_InputFormControls>
</wssuc:InputFormSection>
<wssuc:InputFormSection Title="<%$Resources:wss,scsignup_siteaddress_title%>" runat="server">
<Template_Description>
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,scsignup_siteaddress_desc%>" EncodeMethod='HtmlEncodeAllowSimpleTextFormatting'/>
<asp:Label id="LabelURLPrefix" runat="server"/><SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,scsignup_siteaddress_desc2%>" EncodeMethod='HtmlEncodeAllowSimpleTextFormatting'/>
</Template_Description>
<Template_InputFormControls>
<wssuc:InputFormControl runat="server"
LabelText="<%$Resources:wss,scsignup_sitename_label%>"
>
<Template_Control>
<table cellspacing="0" border="0" cellpadding="0" dir="ltr">
<TR>
<TD nowrap class="ms-authoringcontrols" style="padding-right:2px">
<asp:Label id="LabelSiteNamePrefix" runat="server"/>
</TD>
<asp:PlaceHolder id="PanelPrefix" runat="server">
<TD class="ms-authoringcontrols">
<asp:DropDownList ID="DdlPrefix" Runat="server"></asp:DropDownList>
</TD>
<TD class="ms-authoringcontrols" style="padding-left:2px; padding-right:2px">/</TD>
</asp:PlaceHolder>
<TD class="ms-authoringcontrols">
<wssawc:InputFormTextBox Title="<%$Resources:wss,scsignup_TxtSiteName_Title%>" class="ms-input" ID="TxtSiteName" Columns="18" Runat="server" MaxLength=128 />
</TD>
</TR>
</table>
<wssawc:InputFormRequiredFieldValidator id="ReqValSiteName" runat="server"
BreakBefore=false
ErrorMessage="<%$Resources:wss,scsignup_webfield%>"
ControlToValidate="TxtSiteName"/>
<wssawc:InputFormCustomValidator id="CusValSiteName" runat="server"
ClientValidationFunction="SiteAddressValidate"
ErrorMessage="<%$Resources:wss,scsignup_invalidurl%>"
BreakBefore=false
ControlToValidate="TxtSiteName"/>
</Template_Control>
</wssuc:InputFormControl>
</Template_InputFormControls>
</wssuc:InputFormSection>
<wssuc:TemplatePickerControl id="InputFormTemplatePickerControl" runat="server"
ShowSubWebOnly="false" ShowCustomTemplates="false" />
<asp:PlaceHolder id="PanelSecondaryContact" runat="server">
<wssuc:InputFormSection Title="<%$Resources:wss,scsignup_admins_title%>"
Description="<%$Resources:wss,scsignup_admins_desc%>"
runat="server">
<Template_InputFormControls>
<wssuc:InputFormControl runat="server"
LabelText="<%$Resources:wss,scsignup_admins_label%>">
<Template_Control>
<wssawc:PeopleEditor
id="PickerAdmins"
AllowEmpty=false
ValidatorEnabled="true"
runat="server"
SelectionSet="User"
/>
</Template_Control>
</wssuc:InputFormControl>
</Template_InputFormControls>
</wssuc:InputFormSection>
</asp:PlaceHolder>
<SharePoint:DelegateControl runat="server" Id="DelctlCreateSiteCollectionPanel" ControlId="CreateSiteCollectionPanel1" Scope="Farm" />
<wssuc:ButtonSection runat="server">
<Template_Buttons>
<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%>"/>
</Template_Buttons>
</wssuc:ButtonSection>
</table>
</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:

using System;
using Lapointe.SharePoint.SiteCollectionProvisioner.CreateSiteWebService;
using System.Web.UI.WebControls;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint;
using System.Web;
using Microsoft.SharePoint.Utilities;
using System.Collections.Specialized;
using System.Web.UI;

namespace Lapointe.SharePoint.SiteCollectionProvisioner.ApplicationPages
{
public class CreateSitePage : LayoutsPageBase
{
private const string KEY_CALLED_FROM_OTHER_PRODUCT = "CalledFromOtherProduct";
private const string KEY_DATA_FROM_OTHER_PRODUCT = "Data";
private const string KEY_PORTAL_NAME = "PortalName";
private const string KEY_PORTAL_URL = "PortalUrl";
private const string KEY_PREFIX = "SscPrefix";
private const string KEY_REQUIRE_SECONDARY_CONTACT = "RequireSecondaryContact";
private const string KEY_RETURN_URL = "ReturnUrl";
private const string KEY_TITLE = "Title";
private const string KEY_URLNAME = "UrlName";
private const string KEY_EMAIL = "Email";
private const string KEY_TEMPLATE = "Template";

protected Button BtnCreate;
protected RadioButton CreateDLFalse;
protected RadioButton CreateDLTrue;
protected DropDownList DdlPrefix;
protected DelegateControl DelctlCreateSiteCollectionPanel;
protected TemplatePicker InputFormTemplatePickerControl;
protected Label LabelSiteNamePrefix;
protected Label LabelURLPrefix;
protected PlaceHolder PanelPrefix;
protected PlaceHolder PanelSecondaryContact;
protected PeopleEditor PickerAdmins;
protected InputFormRequiredFieldValidator ReqPickerPeople;
protected TextBox TxtDescription;
protected TextBox TxtDLAlias;
protected TextBox TxtSiteName;
protected TextBox TxtTitle;


#region Properties

/// <summary>
/// Gets the rights required.
/// </summary>
/// <value>The rights required.</value>
protected override SPBasePermissions RightsRequired
{
get
{
return SPBasePermissions.CreateSSCSite;
// Depending on your custom logic it may be necessary to return something other than CreateSSCSite.
//return SPBasePermissions.EmptyMask;
}
}
#endregion

#region Event Handlers

/// <summary>
/// Handles the Click event of the BtnCreate control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
protected void BtnCreate_Click(object sender, EventArgs e)
{
if (!IsValid)
return;

string managedPath = (string)ViewState[KEY_PREFIX];
if (managedPath == null)
{
managedPath = DdlPrefix.SelectedItem.Value;
}
string siteUrl = managedPath + "/" + TxtSiteName.Text.Trim();

if (siteUrl[0] != '/')
{
siteUrl = "/" + siteUrl;
}
if (siteUrl.Length > 1024)
{
siteUrl = siteUrl.Substring(0, 1024);
}
Uri rootUri = SPAlternateUrl.ContextUri;
SPSite currentSite = SPContext.Current.Site;
SPWeb currentWeb = SPContext.Current.Web;

Uri siteUri = new Uri(rootUri, siteUrl);
siteUrl = siteUri.ToString();

string siteTitle = TxtTitle.Text.Trim();
string siteDescription = TxtDescription.Text.Trim();
uint templateLocaleId = uint.Parse(InputFormTemplatePickerControl.SelectedWebLanguage);

string ownerLoginName;
string ownerName = null;
string ownerEmail = currentWeb.CurrentUser.Email;
if (currentWeb.CurrentUser.ID != currentSite.SystemAccount.ID)
{
ownerLoginName = currentWeb.CurrentUser.LoginName;
ownerName = currentWeb.CurrentUser.Name;
}
else
{
ownerLoginName = Utilities.CurrentUserIdentity.Name;
}

bool hasSecondaryContact = false;
string secondaryContactLogin = null;
string secondaryContactName = null;
string secondaryContactEmail = null;
SPUserInfo[] infoArray = null;
bool requireSecondaryContact = (bool)ViewState[KEY_REQUIRE_SECONDARY_CONTACT];
if (requireSecondaryContact)
{
// We need to make sure that if a secondary contact is required that the user actually provided one.
int count = PickerAdmins.ResolvedEntities.Count;
if (count == 0)
{
throw new SPException(GetResourceString("scsignup_admins_error", new object[0]));
}
infoArray = new SPUserInfo[count];
count = 0;

foreach (PickerEntity pickerEntity in PickerAdmins.ResolvedEntities)
{
infoArray[count].LoginName = pickerEntity.Key;
infoArray[count].Email = (string)pickerEntity.EntityData[KEY_EMAIL];
infoArray[count].Name = pickerEntity.DisplayText;
infoArray[count].Notes = "";

if (!hasSecondaryContact)
{
if (!pickerEntity.Key.Equals(ownerLoginName, StringComparison.CurrentCultureIgnoreCase))
{
hasSecondaryContact = true;
secondaryContactLogin = infoArray[count].LoginName;
secondaryContactEmail = infoArray[count].Email;
secondaryContactName = infoArray[count].Name;
}
}
count++;
}
if (!hasSecondaryContact)
throw new SPException(GetResourceString("scsignup_admins_error", new object[0]));
}

string portalUrl;
string portalName;

if ((ViewState[KEY_PORTAL_NAME] != null) && (ViewState[KEY_PORTAL_URL] != null))
{
portalUrl = (string)ViewState[KEY_PORTAL_URL];
portalName = (string)ViewState[KEY_PORTAL_NAME];
}
else
{
// Comment out the following if you don't want the portal URL and name to be set based on the web application root site.
portalUrl = currentSite.PortalUrl;
portalName = currentSite.PortalName;
}

string strWebTemplate = "";
if (!string.IsNullOrEmpty(ViewState[KEY_TEMPLATE] as string))
{
strWebTemplate = (string)ViewState[KEY_TEMPLATE];
}
else if ((InputFormTemplatePickerControl.SelectedWebTemplate != null) &&
(InputFormTemplatePickerControl.SelectedWebTemplate.Length <= 127))
{
strWebTemplate = InputFormTemplatePickerControl.SelectedWebTemplate;
}

bool calledFromOtherProduct = (bool)ViewState[KEY_CALLED_FROM_OTHER_PRODUCT];
string returnUrl;
if (calledFromOtherProduct)
{
returnUrl = (string) ViewState[KEY_RETURN_URL];
returnUrl = returnUrl + "?Data=" +
HttpUtility.UrlEncode((string) ViewState[KEY_DATA_FROM_OTHER_PRODUCT]) + "&SiteUrl=" +
HttpUtility.UrlEncode(siteUri.ToString());
}
else
{
returnUrl = siteUrl;
}

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

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

try
{
using (SPSite site = new SPSite(siteUrl))
using (SPWeb rootWeb = site.RootWeb)
{
site.AllowUnsafeUpdates = true;
rootWeb.AllowUnsafeUpdates = true;

if (requireSecondaryContact)
{
// Add additional users to the site.
rootWeb.SiteUsers.AddCollection(infoArray);

foreach (SPUser user in rootWeb.SiteUsers)
{
if (user.ID == site.SystemAccount.ID)
continue;

if (!user.IsSiteAdmin)
{
user.IsSiteAdmin = true;
user.Update();
}
}
}
// Create the default Members, Owners, and Visitors groups.
rootWeb.CreateDefaultAssociatedGroups(ownerLoginName, secondaryContactLogin, string.Empty);

// Save the site directory data.
SaveSiteDirectoryData(DelctlCreateSiteCollectionPanel, site);

// Link to the main portal
site.PortalUrl = portalUrl;
site.PortalName = portalName;

// Apply the selected web template
rootWeb.ApplyWebTemplate(strWebTemplate);

//TODO: add any additional custom logic to activate features based on user provided data.
}
}
catch (Exception ex)
{
Logger.WriteException(ex, string.Format("Failed to update site collection \"{0}\" (the site collection was created).", siteUrl));
throw;
}
Logger.WriteSuccessAudit(string.Format("Successfully created and updated site collection \"{0}\"", siteUrl));

});

operation.End(returnUrl, SPRedirectFlags.Static, Context, null);
}
}

/// <summary>
/// Raises the <see cref="E:Load"/> event.
/// </summary>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
protected override void OnLoad(EventArgs e)
{
if (!Utilities.CurrentUserIdentity.IsAuthenticated)
{
// Don't allow anonymous access.
SPUtility.SendAccessDeniedHeader(new UnauthorizedAccessException());
}
if (!SPUtility.OriginalServerRelativeRequestUrl.StartsWith("/_layouts/"))
{
// Make sure we're running from the root site collection's _layouts folder.
Utilities.SendResponse(Response, 403, "403 FORBIDDEN");
}

base.OnLoad(e);

SPWebApplication webApplication = SPContext.Current.Site.WebApplication;

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


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

SPAlternateUrl responseUrl = webApplication.AlternateUrls.GetResponseUrl(SPUrlZone.Default);
bool requireSecondaryContact = webApplication.RequireContactForSelfServiceSiteCreation;

if (SPContext.Current.Site.HostHeaderIsSiteName)
{
if (responseUrl != null)
{
throw new SPException(
SPResource.GetString("SscNotAvailableOnHostHeaderSite", new object[] { responseUrl.IncomingUrl }));
}
throw new SPException(SPResource.GetString("SscIsNotEnabled", new object[0]));
}

if (!Page.IsPostBack)
{
// Store any passed in variables for use during postback.
ViewState[KEY_REQUIRE_SECONDARY_CONTACT] = requireSecondaryContact;

if ((Request[KEY_DATA_FROM_OTHER_PRODUCT] == null) || (Request[KEY_RETURN_URL] == null))
{
ViewState[KEY_CALLED_FROM_OTHER_PRODUCT] = false;
}
else
{
ViewState[KEY_CALLED_FROM_OTHER_PRODUCT] = true;
ViewState[KEY_DATA_FROM_OTHER_PRODUCT] = Request[KEY_DATA_FROM_OTHER_PRODUCT];
ViewState[KEY_RETURN_URL] = Request[KEY_RETURN_URL];
}

if (!string.IsNullOrEmpty(Request[KEY_PORTAL_NAME]))
{
ViewState[KEY_PORTAL_NAME] = Request[KEY_PORTAL_NAME];
}

if (!string.IsNullOrEmpty(Request[KEY_PORTAL_URL]))
{
ViewState[KEY_PORTAL_URL] = Request[KEY_PORTAL_URL];
}

if (!string.IsNullOrEmpty(Request[KEY_URLNAME]))
{
TxtSiteName.Text = Request.QueryString[KEY_URLNAME].Trim();
//TxtSiteName.ReadOnly = true;
}

if (!string.IsNullOrEmpty(Request[KEY_TITLE]))
{
TxtTitle.Text = Request.QueryString[KEY_TITLE].Trim();
//TxtTitle.ReadOnly = true;
}

if (!string.IsNullOrEmpty(Request[KEY_TEMPLATE]))
{
ViewState[KEY_TEMPLATE] = Request[KEY_TEMPLATE];
InputFormTemplatePickerControl.Visible = false;
}


string passedInPrefixName = Request[KEY_PREFIX];
string defaultPrefixName = null;
StringCollection wildcardPrefixNames = new StringCollection();


foreach (SPPrefix prefix in webApplication.Prefixes)
{
if (prefix.PrefixType != SPPrefixType.WildcardInclusion)
continue;

wildcardPrefixNames.Add(prefix.Name);
defaultPrefixName = prefix.Name;
}
if (wildcardPrefixNames.Count == 0)
{
throw new SPException(SPResource.GetString("NoInclusionDefinedForSsc", new object[0]));
}
if (wildcardPrefixNames.Count == 1)
{
ViewState[KEY_PREFIX] = defaultPrefixName;
PanelPrefix.Visible = false;
LabelSiteNamePrefix.Text = SPHttpUtility.HtmlEncode(responseUrl.IncomingUrl.TrimEnd('/') + "/" + defaultPrefixName + "/");
LabelURLPrefix.Text = LabelSiteNamePrefix.Text;
}
else
{
foreach (string prefixName in wildcardPrefixNames)
{
if (prefixName.Length != 0)
{
DdlPrefix.Items.Add(new ListItem(prefixName, prefixName));
}
else
DdlPrefix.Items.Add(new ListItem(SPResource.GetString("RootNone", new object[0]), prefixName));
}
LabelSiteNamePrefix.Text = SPHttpUtility.HtmlEncode(responseUrl.IncomingUrl.TrimEnd('/') + "/");
LabelURLPrefix.Text = LabelSiteNamePrefix.Text + wildcardPrefixNames[0] + "/";
}
if (!string.IsNullOrEmpty(passedInPrefixName) && wildcardPrefixNames.Contains(passedInPrefixName))
{
ViewState[KEY_PREFIX] = passedInPrefixName;
PanelPrefix.Visible = false;
LabelSiteNamePrefix.Text = SPHttpUtility.HtmlEncode(responseUrl.IncomingUrl.TrimEnd('/') + "/" + passedInPrefixName + "/");
LabelURLPrefix.Text = LabelSiteNamePrefix.Text;
}

if (!requireSecondaryContact)
{
PanelSecondaryContact.Visible = false;
}

}
if (requireSecondaryContact)
{
SPPrincipalSource principalSource = PickerAdmins.PrincipalSource;
PickerAdmins.PrincipalSource = principalSource & ~SPPrincipalSource.UserInfoList;
}
}

#endregion

#region Helper Methods

/// <summary>
/// Saves the site directory data.
/// </summary>
/// <param name="delegateControl">The delegate control.</param>
/// <param name="site">The site.</param>
internal void SaveSiteDirectoryData(DelegateControl delegateControl, SPSite site)
{
if (site == null)
return;

foreach (Control control in delegateControl.Controls)
{
IFormDelegateControlSource source = control as IFormDelegateControlSource;
if (source == null)
continue;

source.OnFormSave(site);
}
}

#endregion

}

}

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:

using System;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using System.Net;
using System.Web.Services;

namespace Lapointe.SharePoint.SiteCollectionProvisioner.WebServices
{
[WebService(Namespace = "http://schemas.thelapointes.com/sharepoint/soap/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class CreateSiteService : WebService
{
public CreateSiteService() {}

[WebMethod]
public void CreateSite(string webAppUrl,
string databaseHint,
string quotaHint,
string siteUrl,
string title,
string description,
uint templateLocaleId,
string ownerLogin,
string ownerName,
string ownerEmail,
string secondaryContactLogin,
string secondaryContactName,
string secondaryContactEmail)
{
try
{
SPSecurity.RunWithElevatedPrivileges(
delegate
{
Utilities.CreateSite(new Uri(webAppUrl), databaseHint, quotaHint, siteUrl,
title, description, templateLocaleId,
ownerLogin, ownerName, ownerEmail,
secondaryContactLogin,
secondaryContactName,
secondaryContactEmail);
});
}
catch (Exception ex)
{

Logger.WriteException(ex);
}

}
}
}

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:

using System;
using Microsoft.SharePoint.Administration;
using System.Security.Permissions;
using System.Security.Principal;
using System.Web;
using Microsoft.SharePoint;

namespace Lapointe.SharePoint.SiteCollectionProvisioner
{
public class Utilities
{


/// <summary>
/// Gets the current user identity.
/// </summary>
/// <value>The current user identity.</value>
public static IIdentity CurrentUserIdentity
{
[SecurityPermission(SecurityAction.Assert, Flags = SecurityPermissionFlag.ControlPrincipal)]
get
{
if (HttpContext.Current != null)
{
if (HttpContext.Current.User == null)
{
return new GenericIdentity(string.Empty);
}
return HttpContext.Current.User.Identity;
}
return WindowsIdentity.GetCurrent();
}
}

/// <summary>
/// Sends an HTTP response code to the browser.
/// </summary>
/// <param name="response">The HTTP Response object.</param>
/// <param name="code">The response code to send.</param>
/// <param name="body">The body text to send.</param>
public static void SendResponse(HttpResponse response, int code, string body)
{
HttpContext current = HttpContext.Current;
bool? responseEnded = current.Items["ResponseEnded"] as bool?;
if (!responseEnded.HasValue || !responseEnded.Value)
{
current.Items["ResponseEnded"] = true;
response.StatusCode = code;
response.Clear();
if (body != null)
{
response.Write(body);
}
response.End();
}
}


/// <summary>
/// Creates the site collection.
/// </summary>
/// <param name="webAppUri">The web app URI.</param>
/// <param name="databaseHint">The database hint.</param>
/// <param name="quotaHint">The quota hint.</param>
/// <param name="siteUrl">The site URL.</param>
/// <param name="title">The title.</param>
/// <param name="description">The description.</param>
/// <param name="nLCID">The n LCID.</param>
/// <param name="ownerLogin">The owner login.</param>
/// <param name="ownerName">Name of the owner.</param>
/// <param name="ownerEmail">The owner email.</param>
/// <param name="contactLogin">The contact login.</param>
/// <param name="contactName">Name of the contact.</param>
/// <param name="contactEmail">The contact email.</param>
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)
{
Logger.WriteInformation(string.Format("Creating site collection \"{0}\".", siteUrl));

SPWebApplication webApp = SPWebApplication.Lookup(webAppUri);

SPContentDatabase targetDb = GetTargetDatabase(webApp, databaseHint);
SPQuotaTemplate quota = GetQuotaTemplate(webApp, quotaHint);

SPSite site = null;
try
{
if (targetDb == null)
{
// We don't have a specific database so just let SP figure out where to put it.
site = webApp.Sites.Add(siteUrl, title, description, nLCID, null, ownerLogin, ownerName, ownerEmail,
contactLogin, contactName, contactEmail, false);
}
else
{
if (targetDb.CurrentSiteCount == targetDb.MaximumSiteCount)
throw new SPException(string.Format("The database {0} has reached its maximum site count and cannot be added to.", targetDb.Name));

site = targetDb.Sites.Add(siteUrl, title, description, nLCID, null, ownerLogin, ownerName,
ownerEmail, contactLogin, contactName, contactEmail, false);

}

if (quota != null)
{
site.Quota = quota;
}
}
catch (Exception ex)
{
Logger.WriteException(ex, string.Format("Failed to create site collection \"{0}\".", siteUrl));
throw;
}
finally
{
if (site != null)
site.Dispose();
}
Logger.WriteSuccessAudit(string.Format("Successfully created site collection \"{0}\"", siteUrl));
}

/// <summary>
/// Gets the target database that matches the specified prefix name within the given web application.
/// </summary>
/// <param name="webApp">The web app.</param>
/// <param name="databaseHint">Name of the prefix.</param>
/// <returns></returns>
public static SPContentDatabase GetTargetDatabase(SPWebApplication webApp, string databaseHint)
{
SPContentDatabase targetDb = default(SPContentDatabase);

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

foreach (SPContentDatabase db in webApp.ContentDatabases)
{
if (db.Name.ToLower().Contains(databaseHint.ToLower()) && db.MaximumSiteCount > db.CurrentSiteCount)
{
targetDb = db;
break;
}
}

return targetDb;
}

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

SPQuotaTemplate quota = default(SPQuotaTemplate);
SPQuotaTemplateCollection quotaColl = SPFarm.Local.Services.GetValue<SPWebService>("").QuotaTemplates;

foreach (SPQuotaTemplate q in quotaColl)
{
if (q.Name.ToLower().Contains(quotaHint.ToLower()))
{
quota = q;
break;
}
}

if (quota == default(SPQuotaTemplate))
quota = quotaColl[webApp.DefaultQuotaTemplate];

return quota;
}



}
}

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)

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

8Jul/090

Download My Custom Extensions Source From CodePlex

I've been putting this off for a long time but I decided that it was time to push my source code for my custom STSADM commands and PowerShell CmdLets to CodePlex.  You can find the project here: http://stsadm.codeplex.com/.

Note that if you want to download the latest tested release you should still do so from my downloads page on this blog but if you want to see the version history or get some checked in changes that are not yet released (think beta) then feel free to download from the CodePlex project.  Ultimately I think this will make it easier for people to see the specific changes I've made from one build to another and thus make it easier to decide whether they should re-deploy the latest version.

21Dec/0810

Initial Release of My SharePoint PowerShell Cmdlets

Update 4/25/2009: I’ve removed the “-gl” suffix from all my cmdlets - any examples using the -gl in the cmdlet name should be updated.

If you’ve been following my blog you’ll remember that I recently pushed out a “beta” build of some SharePoint PowerShell Cmdlets for some initial feedback and reviews.  We’ll I’ve gone ahead and incorporated the feedback that I received and I am now officially releasing the first version of my cmdlets.  The number of cmdlets has not changed from the beta build – they’ve just been cleaned up and enhanced.  I will slowly start documenting each of the existing cmdlets (there are currently 11) and hopefully be adding new ones over time.

I’ve added links to the top of my blog for quick download of either the source code or the MOSS or WSS builds of the setup packages.  At present there are no differences between the MOSS and WSS builds with the exception of my STSADM extensions which are included in the package and are a required dependency.  I’ve also updated my main index page so that it now also includes the available PowerShell commands – just select what you would like to look at via the dropdown box:

SNAGHTML19e302c5

To install the cmdlets download the MOSS or WSS installer from the links at the top of this page (or in the right-hand column).  Once downloaded run the installer from your MOSS server using your MOSS admin account.  You will see the following screens:

  1. The initial welcome screen: 
    image[10]
  2. The EULA screen – you must accept to continue (you can find a copy for future reference in the install directory): 
    SNAGHTML19de061f
  3. The install location – by default the files will be put at “C:\Program Files\Gary Lapointe\PowerShell Commands for SharePoint\”: 
    image[20]
  4. Optionally install my custom STSADM extensions – the extensions are a required pre-requisite but I make their install conditional so that if you have already installed the extensions you can avoid having to re-install them.  This is particularly beneficial if you are installing the cmdlets on more than one machine in the farm – you only have to install the extensions in the farm once as they will be pushed out to all of the servers by SharePoint: 
    SNAGHTML19debba8
  5. Confirm that you are ready to install: 
    SNAGHTML19df020a
  6. The files are then installed
     

Once the installation has completed you will be presented with the contents of the ReadMe.rtf file (which can be found in the install directory).  I’ve included the contents of that file below for reference:

To automatically load the snapin every time you start PowerShell create a shortcut to PowerShell passing in the -psconsolefile parameter like so:

%SystemRoot%\system32\WindowsPowerShell\v1.0\Powershell.exe –psconsolefile "C:\Program Files\Gary Lapointe\PowerShell Commands for SharePoint\Console.psc1"

The contents of the Console.psc1 file can be seen below.

<?xml version="1.0" encoding="utf-8"?>
<PSConsoleFile ConsoleSchemaVersion="1.0">
  <PSVersion>1.0</PSVersion>
  <PSSnapIns>
    <PSSnapIn Name="Lapointe.SharePoint.PowerShell.Commands" />
  </PSSnapIns>
</PSConsoleFile>

Or run the following command or add the following command to your profile script:

    add-pssnapin -name "Lapointe.SharePoint.PowerShell.Commands"

To see the help file for any of the commands type the following (where -detailed and -full are optional):

    help <command name> [<-detailed | -full>]

For example, the following command will return the full help for the get-spsite command:

    help get-spsite -full

Here’s a quick demonstration showing some of the power of one of these cmdlets, the Get-SPSite cmdlet:

image

Looking at the example above you can see I was able to quickly get a list of all Site Collections using “$sites = get-spsite –url *”.  I then output the results to the display to see what got returned.  To get some different columns I then passed in the $sites variable to the Select-Object (select) cmdlet and had it return back the Url and Usage.Storage properties so that I can see the size of all the size collections in the farm.  I could take this further and do some filtering if necessary but I’ll leave that exercise to you.

Keep an eye on my blog for more online documentation about each of the cm dlets along with lots of useful examples (hopefully :) ).