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

18Dec/0911

Creating a SharePoint 2010 Site Structure Using PowerShell

In a previous post I detailed how to use PowerShell to perform what would be otherwise done using PSConfig to create an initial SharePoint Farm. In this post I will continue the example and show how to create your web applications using a simple XML configuration file and a reusable script.

Like the previous example I have a very basic XML file that defines my web application structure. In this example I've included not only the web application and application pool but also the content databases and site collections, along with the SharePoint Designer settings. Consider the XML and corresponding PowerShell a starting place to extend further if needed by adding elements for managed paths, quota templates, sites and even lists. Here's the XML which I store in a file called WebAppConfigurations.xml:

<WebApplications>
<WebApplication Name="SharePoint Portal (80)"
DefaultTimeZone="12"
DefaultQuotaTemplate="Portal"
AllowAnonymous="false"
AuthenticationMethod="NTLM"
HostHeader="portal"
Path="c:\sharepoint\webs\portal"
Port="80"
LoadBalancedUrl="http://portal"
Ssl="false">
<ApplicationPool Name="SharePoint Portal App Pool"
Account="sp2010\spportalapppool" />
<SPDesigner AllowDesigner="true" AllowRevertFromTemplate="true"
AllowMasterPageEditing="true" ShowURLStructure="true" />
<ContentDatabases>
<ContentDatabase Server="spsql1"
Name="SharePoint_Content_Portal1"
MaxSiteCount="100" WarningSiteCount="80"
Default="true">
<SiteCollections>
<SiteCollection Name="Portal"
Description=""
Url="http://portal"
LCID="1033"
Template="SPSPORTAL#0"
OwnerLogin="sp2010\siteowner1"
OwnerEmail="siteowner1@sp2010.com"
SecondaryLogin="sp2010\spadmin"
SecondaryEmail="spadmin@sp2010.com">
</SiteCollection>
</SiteCollections>
</ContentDatabase>
</ContentDatabases>
</WebApplication>
</WebApplications>

Note that you could easily adapt the file by having the <WebApplications /> element be a child of the <Farm /> element shown in my previous post resulting in a single configuration file rather than multiple files. One thing to note is that I'm not storing the password for the application pool account which I assume exists - the password will be asked for when the script runs.

Let's take a look at the script that does all the work:

function Start-WebApplicationsBuild(
    [string]$settingsFile = "Configurations.xml") {
    [xml]$config = Get-Content $settingsFile

    #Creating individual web applications
    $config.WebApplications.WebApplication | ForEach-Object {
        $webAppConfig = $_
        $webApp = New-WebApplication $webAppConfig

        #Configuring SharePoint Designer Settings
        $spd = $webAppConfig.SPDesigner
        $allowRevert = ([bool]::Parse($spd.AllowRevertFromTemplate))
        $allowMasterEdit = ([bool]::Parse($spd.AllowMasterPageEditing))
        Write-Host "Setting SP Designer settings..."
        $webApp | Set-SPDesignerSettings `
            -AllowDesigner:([bool]::Parse($spd.AllowDesigner)) `
            -AllowRevertFromTemplate:$allowRevert `
            -AllowMasterPageEditing:$allowMasterEdit `
            -ShowURLStructure:([bool]::Parse($spd.ShowURLStructure))

        $webAppConfig.ContentDatabases.ContentDatabase | ForEach-Object {
            #Creating content database
            Write-Host "Creating content database $($_.Name)..."
            $db = New-SPContentDatabase -Name $_.Name `
                -WebApplication $webApp `
                -DatabaseServer $_.Server `
                -MaxSiteCount $_.MaxSiteCount `
                -WarningSiteCount $_.WarningSiteCount

            $_.SiteCollections.SiteCollection | ForEach-Object {
                #Creating site collection
                Write-Host "Creating site collection $($_.Url)..."
                $gc = Start-SPAssignment
                $site = $gc | New-SPSite `
                    -Url $_.Url `
                    -ContentDatabase $db `
                    -Description $_.Description `
                    -Language $_.LCID `
                    -Name $_.Name `
                    -Template $_.Template `
                    -OwnerAlias $_.OwnerLogin `
                    -OwnerEmail $_.OwnerEmail `
                    -SecondaryOwnerAlias $_.SecondaryLogin `
                    -SecondaryEmail $_.SecondaryEmail
                Stop-SPAssignment -SemiGlobal $gc
            }
        }
    }
}

function New-WebApplication([System.Xml.XmlElement]$webAppConfig) {
    $poolAccount = $null
    $tempAppPool = $null
    $poolName = $webAppConfig.ApplicationPool.Name
    if ([Microsoft.SharePoint.Administration.SPWebService]::ContentService.ApplicationPools.Count -gt 0) {
        $tempAppPool = [Microsoft.SharePoint.Administration.SPWebService]::ContentService.ApplicationPools | ? {$_.Name -eq $poolName}
    }
    if ($tempAppPool -eq $null) {
        Write-Host "Getting $($webAppConfig.ApplicationPool.Account) account for application pool..."
        $accountCred = Get-Credential $webAppConfig.ApplicationPool.Account
        $poolAccount = (Get-SPManagedAccount -Identity $accountCred.Username -ErrorVariable err -ErrorAction SilentlyContinue)
        if ($err) {
            $poolAccount = New-SPManagedAccount -Credential $accountCred
        }
    }

    $allowAnon = [bool]::Parse($webAppConfig.AllowAnonymous.ToString())
    $ssl = [bool]::Parse($webAppConfig.Ssl.ToString())

    $db = $null
    if ($webAppConfig.ContentDatabases.ChildNodes.Count -gt 1) {
        $db = $webAppConfig.ContentDatabases.ContentDatabase | `
            where {$_.Default -eq "true"}
        if ($db -is [array]) {
            $db = $db[0]
        }
    } else {
        $db = $webAppConfig.ContentDatabases.ContentDatabase
    }

    #Create the web application
    Write-Host "Creating web application $($webAppConfig.Name)..."
    $webApp = New-SPWebApplication -SecureSocketsLayer:$ssl `
        -AllowAnonymousAccess:$allowAnon `
        -ApplicationPool $poolName `
        -ApplicationPoolAccount $poolAccount `
        -Name $webAppConfig.Name `
        -AuthenticationMethod $webAppConfig.AuthenticationMethod `
        -DatabaseServer $db.DatabaseServer `
        -DatabaseName $db.DatabaseName `
        -HostHeader $webAppConfig.HostHeader `
        -Path $webAppConfig.Path `
        -Port $webAppConfig.Port `
        -Url $webAppConfig.LoadBalancedUrl `
        -ErrorVariable err

    return $webApp
}

I've put the script in two different functions with Start-WebApplicationsBuild being the primary function that is called by the logged in user. The other function, New-WebApplication, is just there for readability (I wanted to separate out the code that created the application pool and web application itself). Note that, like in my previous post, I use a more complex version of this script which has the various elements broken out into many different shared helper functions and considerably more tracing and error handling added - this script is a fairly simplistic version which lets you focus on the core SharePoint 2010 PowerShell stuff without polluting the code with lots of plumbing.

With this script and XML file structure you can create as many web applications, content databases, and site collections as needed by only modifying the XML file - the script will support any number of each. One thing to be careful of - make sure you have only one <ContentDatabase /> element with a Default attribute set to "true" (this is the database that will be created when the web application is created - you may have as many <ContentDatabase /> elements as needed but you need at least one with a Default value of true).

Hopefully this script proves useful to anyone who needs to automatically create their SharePoint 2010 site structure. Stay tuned for the next piece of the scripts which will cover provisioning service applications.

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

30Apr/091

Creating Default Site Groups After Creating Site Collections Using STSADM

I got an email from Jennifer Davis today asking why, when she ran my gl-createsiteindb command, did the default site groups not show up in the site collection, specifically the “<site name> Members”, “<site name> Owners”, and “<site name> Visitors” groups.  Upon digging further she realized that this behavior was not limited to my command as the out-of-the-box createsite and createsiteinnewdb commands exhibited the same behavior.

Basically what’s happening is that if you create the site collection via the browser an additional method call gets made on the root web site of the site collection: SPWeb.CreateDefaultAssociatedGroups.  For whatever reason this method call is not made when using STSADM and as my gl-createsiteindb command just mimics the createsite command I too did not make the necessary method call.  Well, I agree with Jennifer that this is just wrong so I decided to go ahead and modify my code so that those default site groups would get created.  Fortunately it was a really simple change - here’s the code that I added to the command:

   1: if (!string.IsNullOrEmpty(webTemplate))
   2: {
   3:     using (SPWeb web = site.RootWeb)
   4:     {
   5:         web.CreateDefaultAssociatedGroups(ownerLogin, secondaryContactLogin, string.Empty);
   6:     }
   7: }

If you’re using any of the existing out of the box commands you can easily achieve the same end result with a couple lines of PowerShell, as the following demonstrates (requires my custom cmdlets):

$url = "http://<site url>"
$primaryOwner = "domain\user"
$secondaryOwner = "domain\user"
$site = Get-SPSite $url
$site.SPBase.RootWeb.CreateDefaultAssociatedGroups($primaryOwner, $secondaryOwner, "")
$site.SPBase.Dispose()

21Mar/0911

Why I don’t use OpenWeb()

This has come up in various conversations recently so I figured I’d write up a short post about it.  When trying to obtain an SPWeb object there are a couple of different options available using members of an SPSite instance.  The first, and more common, is the OpenWeb() method and the second is using the AllWebs[] property collection.  Here’s the problem I have with OpenWeb() (specifically the overload that takes no arguments) – consider the following code:

string url = "http://portal/sites/ActualSite/ChildWeb/GrandChildWeb";
using (SPSite site = new SPSite(url))
using (SPWeb web = site.OpenWeb())
{
    ...
}

In the above code if both ChildWeb and GrandChildWeb actually exist then the OpenWeb() call will return the GrandChild web.  However, lets assume that GrandChildWeb doesn’t exist (or perhaps it was spelled incorrectly), in that case what you will get for the web variable will be ChildWeb (assuming it exists).  The problem is that the user will not get any error indicating that the URL provided was invalid – it simply returns back the first valid web in the URL hierarchy.  In some cases this is exactly what you want (this is common when the URL you have is that of a List or Folder) but generally this is not going to be the case.

Now consider the following code where “Utilities.GetServerRelativeUrl” is a helper method that returns back the server relative URL:

string url = "http://portal/sites/ActualSite/ChildWeb/GrandChildWeb";
using (SPSite site = new SPSite(url))
using (SPWeb web = site.OpenWeb(Utilities.GetServerRelativeUrl(url), true))
{
    ...
}

In the above code if GrandChildWeb does not exist then an ArgumentException will be thrown thus ensuring that the code does not now inadvertently operate on the wrong SPWeb object.  Alternatively you could do the exact same thing using the AllWebs property collection as shown below:

string url = "http://portal/sites/ActualSite/ChildWeb/GrandChildWeb";
using (SPSite site = new SPSite(url))
using (SPWeb web = site.AllWebs[Utilities.GetServerRelativeUrl(url)])
{
    ...
}

I actually prefer using this method when the URL I have is explicitly expected to be the URL of a web.  This helps me during code reviews to do a quick search for all OpenWeb calls thus allowing me to focus my attention on how and where the URL parameter is coming from (so I use OpenWeb() with no arguments whenever I have an arbitrary URL that points to a resource below a web and not the web itself and I use AllWebs for everything else).

So I know what you’re thinking: “But Gary, won’t using a property collection result in all the webs being opened so isn’t using AllWebs less performant?”.  Actually, no, that’s not how AllWebs works – internally the indexer for AllWebs makes a call to the SPSite.OpenWeb(string, bool) method passing in the server relative URL and true to ensure an exact match.  This results in an ArgumentException if the URL specified does not correspond to a web.

In conclusion, use AllWebs when the URL you have is expected to correspond to a web and use OpenWeb when the URL corresponds to a list, folder, or file.

20Jan/096

Working with SPSite(Info) Objects Using PowerShell

One of the first PowerShell cmdlets I built, Get-SPSite, addresses some common issues found with working with SPSite objects.  I struggled with how I could provide a means to quickly and easily get SPSite objects while at the same time helping administrators so they don’t have to worry (as much) about object disposal.  For those that aren’t familiar with the SPSite object (Microsoft.SharePoint.SPSite), it’s the equivalent programmatic element for working with site collections.

What I eventually ended up creating (thanks to some good advice from Harley Green) was a simple wrapper object which encapsulates most of the key properties of the SPSite object thus allowing basic reporting and decision making processing without the need to worry about disposing of the object.  Consider the following code snippet:

[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Sharepoint")
$webapp = [Microsoft.SharePoint.Administration.SPWebApplication]::Lookup("http://portal")
foreach ($site in $webapp.Sites) {
    Write-Host $site.Url
}

The above code results in a memory leak.  If you don’t re-loop through each SPSite object in the collection and dispose of the object by calling the “Dispose()” method you will end up with unmanaged resources left in memory that could eventually cause issues if you do a lot of processing like this (eventually the GC will dispose of the objects but that could take quite some time).

Another option would be to use a different approach to get all the SPSite objects within a web application, an approach that can be used for more dynamic querying of objects and returns back an object that would not require disposal – an SPSiteInfo object.  Here’s an example of how you could do something similar to the above using my Get-SPSite cmdlet:

foreach ($site in get-spsite -url http://portal*) {
    Write-Host $site.Url
}

The one obvious downside of this approach is that the wildcard means that I have to inspect every single site collection within the farm to figure out where there are matches so if you’re looking for performance this definitely isn’t the best approach.  Typically though, we’re more concerned about flexibility and ease of use rather than performance when performing the simple administrative tasks that we’d be looking to perform using PowerShell.

What I like about the approach I put together is that I can now do filtered queries without having to worry about whether or not I disposed of the objects.  Here’s an example of how to find all the site collections within the farm where the storage size is greater than 80% of the quota:

get-spsite -url * | where -filterscript {$_.Usage.Storage -ge $_.Quota.StoragemaximumLevel*.8 -and $_.Quota.StorageMaximumLevel -ne 0} | select Url,@{Name="Storage";expression={$_.Usage.Storage/1MB}}

In the above I can simply call my Get-SPSite cmdlet, filter out all items where the current storage is less than 0.8 of the maximum level if set (StorageMaximumLevel is not 0), and then display the URL and the current size, in megabytes, of the the remaining site collections.

It’s important to remember that the SPSiteInfo object is meant to be read-only as most of the properties are just copies of the variables but there are some exceptions such as the SPRecycleBinItemCollection object returned by the RecycleBin property or the SPFeatureCollection object returned by the Features property.  In general, if you have to call the Update() method of the SPSite object to save your changes then you have to use the actual SPSite object, otherwise you can work directly with the SPSiteInfo object and forego the need to instantiate and dispose of the SPSite object.

Okay, so working with properties is pretty easy and we can do some nice reports using them and even access the web application using the WebApplication property or the webs using the AllWebs collection property (all without having to dispose any of the returned objects – the AllWebs property returns a collection of SPWebInfo objects) but what about when you do need to access the actual SPSite object?  There are two approaches for this: the first is to use the SPBase property which will create a new SPSite instance and store that instance as a private member variable for future access to the property thus avoiding the overhead of creating another instance on subsequent calls; the second is to use the GetSPObject() method which creates a new instance of the SPSite object but does not store a copy so it’s a nice easy way to get an entirely new instance of the actual SPSite object (useful for when you’ve made a change which requires a reload due to caching).  In both cases you are responsible for disposing of the returned object.

The following code snippet shows the SPSiteInfo class:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Management.Automation;
   5: using System.Text;
   6: using Microsoft.SharePoint;
   7: using Microsoft.SharePoint.Administration;
   8: using Microsoft.SharePoint.Workflow;
   9:  
  10: namespace Lapointe.SharePoint.PowerShell.Commands.Proxies
  11: {
  12:     public class SPSiteInfo : ISPInfo
  13:     {
  14:         private List<SPWebInfo> m_AllWebs;
  15:         private SPSite m_Site;
  16:  
  17:         internal SPSiteInfo(SPSite site)
  18:         {
  19:             ID = site.ID;
  20:             AllowRssFeeds = site.AllowRssFeeds;
  21:             AllowUnsafeUpdates = site.AllowUnsafeUpdates;
  22:             ApplicationRightsMask = site.ApplicationRightsMask;
  23:             Audit = site.Audit;
  24:             CatchAccessDeniedException = site.CatchAccessDeniedException;
  25:             CertificationDate = site.CertificationDate;
  26:             ContentDatabase = site.ContentDatabase;
  27:             CurrentChangeToken = site.CurrentChangeToken;
  28:             DeadWebNotificationCount = site.DeadWebNotificationCount;
  29:             ExternalBinaryIds = site.ExternalBinaryIds;
  30:             Features = site.Features;
  31:             HostHeaderIsSiteName = site.HostHeaderIsSiteName;
  32:             HostName = site.HostName;
  33:             IISAllowsAnonymous = site.IISAllowsAnonymous;
  34:             Impersonating = site.Impersonating;
  35:             IsPaired = site.IsPaired;
  36:             LastContentModifiedDate = site.LastContentModifiedDate;
  37:             LastSecurityModifiedDate = site.LastSecurityModifiedDate;
  38:             LockIssue = site.LockIssue;
  39:             Owner = site.Owner;
  40:             Port = site.Port;
  41:             PortalName = site.PortalName;
  42:             PortalUrl = site.PortalUrl;
  43:             Protocol = site.Protocol;
  44:             Quota = site.Quota;
  45:             ReadLocked = site.ReadLocked;
  46:             ReadOnly = site.ReadOnly;
  47:             RecycleBin = site.RecycleBin;
  48:             try
  49:             {
  50:                 RootWeb = new SPWebInfo(site.RootWeb);
  51:             }
  52:             catch (Exception) {}
  53:             SearchServiceInstance = site.SearchServiceInstance;
  54:             SecondaryContact = site.SecondaryContact;
  55:             ServerRelativeUrl = site.ServerRelativeUrl;
  56:             SyndicationEnabled = site.SyndicationEnabled;
  57:             SystemAccount = site.SystemAccount;
  58:             UpgradeRedirectUri = site.UpgradeRedirectUri;
  59:             Url = site.Url;
  60:             Usage = site.Usage;
  61:             try
  62:             {
  63:                 UserAccountDirectoryPath = site.UserAccountDirectoryPath;
  64:             }
  65:             catch (UnauthorizedAccessException) { }
  66:             UserToken = site.UserToken;
  67:             WarningNotificationSent = site.WarningNotificationSent;
  68:             WebApplication = site.WebApplication;
  69:             //WorkflowManager = site.WorkflowManager;
  70:             WriteLocked = site.WriteLocked;
  71:             Zone = site.Zone;
  72:  
  73:         }
  74:  
  75:         /// <summary>
  76:         /// Returns a newly created instance of the object on the first access.  Subsequent accesses will utilize an internal member variable.
  77:         /// The caller is responsible for disposing of the returned object.
  78:         /// </summary>
  79:         /// <value>The SP base.</value>
  80:         public IDisposable SPBase
  81:         {
  82:             get
  83:             {
  84:                 if (m_Site == null)
  85:                     m_Site = new SPSite(ID);
  86:  
  87:                 return m_Site;
  88:             }
  89:         }
  90:  
  91:         /// <summary>
  92:         /// Returns a newly created instance of the object every time without storing an internal member variable for subsequent access.
  93:         /// The caller is responsible for disposing of the returned object.
  94:         /// </summary>
  95:         /// <returns></returns>
  96:         public IDisposable GetSPObject()
  97:         {
  98:             return new SPSite(ID);
  99:         }
 100:  
 101:         public bool AllowRssFeeds { get; internal set; }
 102:         public bool AllowUnsafeUpdates { get; internal set; }
 103:         public List<SPWebInfo> AllWebs
 104:         {
 105:             get
 106:             {
 107:                 if (m_AllWebs != null)
 108:                     return m_AllWebs;
 109:  
 110:                 m_AllWebs = new List<SPWebInfo>();
 111:                 using (SPSite site = new SPSite(ID))
 112:                 {
 113:                     foreach (SPWeb web in site.AllWebs)
 114:                     {
 115:                         try
 116:                         {
 117:                             m_AllWebs.Add(new SPWebInfo(web));
 118:                         }
 119:                         finally
 120:                         {
 121:                             web.Dispose();
 122:                         }
 123:                     }
 124:                 }
 125:                 return m_AllWebs;
 126:             }
 127:         }
 128:         public SPBasePermissions ApplicationRightsMask { get; internal set; }
 129:         public SPAudit Audit { get; internal set; }
 130:         public bool CatchAccessDeniedException { get; internal set; }
 131:         public DateTime CertificationDate { get; internal set; }
 132:         public SPContentDatabase ContentDatabase { get; internal set; }
 133:         public SPChangeToken CurrentChangeToken { get; internal set; }
 134:         public short DeadWebNotificationCount { get; internal set; }
 135:         public SPExternalBinaryIdCollection ExternalBinaryIds { get; internal set; }
 136:         public SPFeatureCollection Features { get; internal set; }
 137:         public bool HostHeaderIsSiteName { get; internal set; }
 138:         public string HostName { get; internal set; }
 139:         public Guid ID { get; internal set; }
 140:         public bool IISAllowsAnonymous { get; internal set; }
 141:         public bool Impersonating { get; internal set; }
 142:         public bool IsPaired { get; internal set; }
 143:         public DateTime LastContentModifiedDate { get; internal set; }
 144:         public DateTime LastSecurityModifiedDate { get; internal set; }
 145:         public string LockIssue { get; internal set; }
 146:         public SPUser Owner { get; internal set; }
 147:         public int Port { get; internal set; }
 148:         public string PortalName { get; internal set; }
 149:         public string PortalUrl { get; internal set; }
 150:         public string Protocol { get; internal set; }
 151:         public SPQuota Quota { get; internal set; }
 152:         public bool ReadLocked { get; internal set; }
 153:         public bool ReadOnly { get; internal set; }
 154:         public SPRecycleBinItemCollection RecycleBin { get; internal set; }
 155:         public SPWebInfo RootWeb { get; internal set; }
 156:         public SPServiceInstance SearchServiceInstance { get; internal set; }
 157:         public SPUser SecondaryContact { get; internal set; }
 158:         public string ServerRelativeUrl { get; internal set; }
 159:         public bool SyndicationEnabled { get; internal set; }
 160:         public SPUser SystemAccount { get; internal set; }
 161:         public Uri UpgradeRedirectUri { get; internal set; }
 162:         public string Url { get; internal set; }
 163:         public SPSite.UsageInfo Usage { get; internal set; }
 164:         public string UserAccountDirectoryPath { get; internal set; }
 165:         public SPUserToken UserToken { get; internal set; }
 166:         public bool WarningNotificationSent { get; internal set; }
 167:         public SPWebApplication WebApplication { get; internal set; }
 168:         //public SPWorkflowManager WorkflowManager { get; internal set; }
 169:         public bool WriteLocked { get; internal set; }
 170:         public SPUrlZone Zone { get; internal set; }
 171:  
 172:     }
 173: }

The following is the code of the core Get-SPSite cmdlet:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Management.Automation;
   4: using Lapointe.SharePoint.PowerShell.Commands.OperationHelpers;
   5: using Lapointe.SharePoint.PowerShell.Commands.Validators;
   6: using Microsoft.SharePoint;
   7: using Microsoft.SharePoint.Administration;
   8: using Lapointe.SharePoint.PowerShell.Commands.Proxies;
   9:  
  10: namespace Lapointe.SharePoint.PowerShell.Commands.SiteCollections
  11: {
  12:     [Cmdlet(VerbsCommon.Get, "SPSite", SupportsShouldProcess=true, DefaultParameterSetName = "Url")]
  13:     public class GetSPSiteCommand : PSCmdletBase
  14:     {
  15:         /// <summary>
  16:         /// Gets or sets the URL.
  17:         /// </summary>
  18:         /// <value>The URL.</value>
  19:         [Parameter(
  20:             ParameterSetName = "Url",
  21:             Mandatory = true,
  22:             Position = 0,
  23:             ValueFromPipeline = true,
  24:             ValueFromPipelineByPropertyName = true,
  25:             HelpMessage = "The URL of the site to return.  Supports wildcards.")]
  26:         [ValidateNotNullOrEmpty]
  27:         [ValidateUrl(true)]
  28:         public string[] Url { get; set; }
  29:  
  30:         
  31:         /// <summary>
  32:         /// Processes the record.
  33:         /// </summary>
  34:         protected override void ProcessRecordEx()
  35:         {
  36:             foreach (string url in Url)
  37:             {
  38:                 if (!WildcardPattern.ContainsWildcardCharacters(url))
  39:                 {
  40:                     string siteUrl = url.TrimEnd('/');
  41:                     using (SPSite site = new SPSite(siteUrl))
  42:                     {
  43:                         WriteObject(new SPSiteInfo(site));
  44:                     }
  45:                 }
  46:                 else
  47:                 {
  48:                     WildcardPattern wildCard = new WildcardPattern(url, WildcardOptions.IgnoreCase);
  49:                     if (SPFarm.Local == null)
  50:                         throw new SPException("The SPFarm object is null.  Make sure you are running as a Farm Administrator.");
  51:  
  52:                     foreach (SPService svc in SPFarm.Local.Services)
  53:                     {
  54:                         if (!(svc is SPWebService))
  55:                             continue;
  56:  
  57:                         foreach (SPWebApplication webApp in ((SPWebService)svc).WebApplications)
  58:                         {
  59:                             for (int i = 0; i < webApp.Sites.Count; i++)
  60:                             {
  61:                                 using (SPSite site = webApp.Sites[i])
  62:                                 {
  63:                                     if (wildCard.IsMatch(site.Url))
  64:                                         WriteObject(new SPSiteInfo(site));
  65:                                 }
  66:                             }
  67:                         }
  68:                     }
  69:                 }
  70:             }
  71:         }
  72:     }
  73: }

The following is the full help for the cmdlet.

PS C:\> get-help get-spsite -full

NAME
    Get-SPSite

SYNOPSIS
    Gets one or more SPSiteInfo objects representing a SharePoint 2007 Site Collection.

SYNTAX
    Get-SPSite [-Url] <String[]> [-WhatIf] [-Confirm] [<CommonParameters>]


DETAILED DESCRIPTION
    Pass in a comma separated list of URLs or a string array of URLs to obtain a collection of SPSiteInfo objects.  The
    se objects do not need to be disposed.

    The SPSiteInfo object that is returned contains almost all of the same properties of the SPSite object but does not
     require disposal and should be generally considered read-only.  You can get to the actual SPSite object by using t
    he SPBase property or the GetSPObject() method.  The SPBase property results in a copy of the SPSite object being p
    ersisted in the SPSiteInfo object for faster access on future calls.  Always remember to dispose of the SPSite obje
    ct if used.  Some collection properties may be directly updated without the need to access the SPSite object.

    Copyright 2008 Gary Lapointe
      > For more information on these PowerShell cmdlets:
      > http://stsadm.blogspot.com/
      > Use of these cmdlets is at your own risk.
      > Gary Lapointe assumes no liability.


PARAMETERS
    -Url <String[]>
        Specifies the URL of the site collection(s) to retrieve. Wildcards are permitted. If you specify multiple URLs,
         use commas to separate the URLs.

        Required?                    true
        Position?                    1
        Default value
        Accept pipeline input?       true (ByValue, ByPropertyName)
        Accept wildcard characters?  false

    -WhatIf


        Required?                    false
        Position?                    named
        Default value
        Accept pipeline input?       false
        Accept wildcard characters?  false

    -Confirm


        Required?                    false
        Position?                    named
        Default value
        Accept pipeline input?       false
        Accept wildcard characters?  false

    <CommonParameters>
        This cmdlet supports the common parameters: -Verbose, -Debug,
        -ErrorAction, -ErrorVariable, and -OutVariable. For more information,
        type, "get-help about_commonparameters".

INPUT TYPE
    String


RETURN TYPE
    Collection of SPSiteInfo objects.


NOTES


        For more information, type "Get-Help Get-SPSite -detailed". For technical information, type "Get-Help Get-SP
        Site -full".

    --------------  Example 1 --------------

    C:\PS>get-spsite -url http://portal


    This example returns back a single SPSiteInfo object.





    --------------  EXAMPLE 2 --------------

    C:\PS>$sites = get-spsite -url http://mysites/*


    This example returns back all My Site site collections under the http://mysites web application.






RELATED LINKS

http://stsadm.blogspot.com

Note that if you receive an exception during the execution of this cmdlet simply pass in the “-debug” parameter in order to display the full stack trace which you can use to either debug yourself or report back to here to help me improve the code.

And finally – if you’ve used this cmdlet (or any others that I’ve provided) to do something cool please post your code here as a comment so that others may benefit and possibly give back some feedback that you yourself could use.

15Jan/097

Recalculating Usage Statistics via STSADM or PowerShell

I was perusing through the SharePoint forums the other day and I came across an issue that someone was having with the usage statistics information for their My Sites site collections.  When they viewed the usage data (~site/_layouts/usage.aspx) they were seeing incorrect information.  I’m not really sure why the numbers were wrong but fixing them turned out to be pretty easy.  There’s a public method called “RecalculateStorageUsed” that, when called, will recalculate the usage statistics for the site collection.  I decided to do some digging within the SharePoint code and what I found was rather interesting – Microsoft created an stsadm command that would allow you to call this method via stsadm for a site collection – but they didn’t publish the command via any config file so even though the code is there, you can’t use it.  I tried to do some more poking around to see if there was a timer job or something that either called this method or one of the internal SPRequest methods that actually does the work but unfortunately I didn’t find anything that would help identify why the numbers weren’t correct.

So, knowing that Microsoft had started the creation of such a command but didn’t finish I decided that I’d go ahead and “finish” it for them – but better, naturally :) .  You can now use my gl-recalculateusage command and pass in various scopes (Farm, WebApplication, or Site) and it will call the aforementioned method for each site collection within the specified scope.

The complete code for the command is included below:

   1: using System;
   2: using System.Text;
   3: using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
   4: using Lapointe.SharePoint.STSADM.Commands.SPValidators;
   5: using System.Collections.Specialized;
   6: using Microsoft.SharePoint;
   7: using Microsoft.SharePoint.Administration;
   8:  
   9: namespace Lapointe.SharePoint.STSADM.Commands.SiteCollectionSettings
  10: {
  11:     /// <summary>
  12:     /// 
  13:     /// </summary>
  14:     public class RecalculateUsage : SPOperation
  15:     {
  16:         /// <summary>
  17:         /// Initializes a new instance of the <see cref="RecalculateUsage"/> class.
  18:         /// </summary>
  19:         public RecalculateUsage()
  20:         {
  21:             SPParamCollection parameters = new SPParamCollection();
  22:             parameters.Add(new SPParam("url", "url", false, null, new SPUrlValidator()));
  23:             parameters.Add(new SPParam("scope", "s", false, "site", new SPRegexValidator("(?i:^Farm$|^WebApplication$|^Site$)")));
  24:  
  25:  
  26:             StringBuilder sb = new StringBuilder();
  27:             sb.Append("\r\n\r\nRecalculates usage statistics for the given site(s).\r\n\r\nParameters:");
  28:             sb.Append("\r\n\t[-scope <Farm | WebApplication | Site>]");
  29:             sb.Append("\r\n\t[-url <url>]");
  30:  
  31:             Init(parameters, sb.ToString());
  32:         }
  33:  
  34:         /// <summary>
  35:         /// Gets the help message.
  36:         /// </summary>
  37:         /// <param name="command">The command.</param>
  38:         /// <returns></returns>
  39:         public override string GetHelpMessage(string command)
  40:         {
  41:             return HelpMessage;
  42:         }
  43:  
  44:         /// <summary>
  45:         /// Executes the specified command.
  46:         /// </summary>
  47:         /// <param name="command">The command.</param>
  48:         /// <param name="keyValues">The key values.</param>
  49:         /// <param name="output">The output.</param>
  50:         /// <returns></returns>
  51:         public override int Execute(string command, StringDictionary keyValues, out string output)
  52:         {
  53:             output = string.Empty;
  54:             Verbose = true;
  55:  
  56:             string scope = Params["scope"].Value.ToLowerInvariant();
  57:  
  58:             SPEnumerator enumerator;
  59:             if (scope == "farm")
  60:             {
  61:                 enumerator = new SPEnumerator(SPFarm.Local);
  62:             }
  63:             else if (scope == "webapplication")
  64:             {
  65:                 enumerator = new SPEnumerator(SPWebApplication.Lookup(new Uri(Params["url"].Value.TrimEnd('/'))));
  66:             }
  67:             else
  68:             {
  69:                 // scope == "site"
  70:                 using (SPSite site = new SPSite(Params["url"].Value.TrimEnd('/')))
  71:                 {
  72:                     Recalculate(site);
  73:                 }
  74:                 return OUTPUT_SUCCESS;
  75:             }
  76:  
  77:             enumerator.SPSiteEnumerated += new SPEnumerator.SPSiteEnumeratedEventHandler(enumerator_SPSiteEnumerated);
  78:             enumerator.Enumerate();
  79:  
  80:             return OUTPUT_SUCCESS;
  81:         }
  82:  
  83:         /// <summary>
  84:         /// Validates the specified key values.
  85:         /// </summary>
  86:         /// <param name="keyValues">The key values.</param>
  87:         public override void Validate(StringDictionary keyValues)
  88:         {
  89:             if (Params["scope"].Validate())
  90:             {
  91:                 Params["url"].IsRequired = true;
  92:                 Params["url"].Enabled = true;
  93:                 if (Params["scope"].Value.ToLowerInvariant() == "farm")
  94:                 {
  95:                     Params["url"].IsRequired = false;
  96:                     Params["url"].Enabled = false;
  97:                 }
  98:  
  99:             }
 100:             base.Validate(keyValues);
 101:         }
 102:  
 103:         /// <summary>
 104:         /// Handles the SPSiteEnumerated event of the enumerator control.
 105:         /// </summary>
 106:         /// <param name="sender">The source of the event.</param>
 107:         /// <param name="e">The <see cref="Lapointe.SharePoint.STSADM.Commands.OperationHelpers.SPEnumerator.SPSiteEventArgs"/> instance containing the event data.</param>
 108:         private void enumerator_SPSiteEnumerated(object sender, SPEnumerator.SPSiteEventArgs e)
 109:         {
 110:             Recalculate(e.Site);
 111:         }
 112:  
 113:         /// <summary>
 114:         /// Recalculates the specified site.
 115:         /// </summary>
 116:         /// <param name="site">The site.</param>
 117:         private void Recalculate(SPSite site)
 118:         {
 119:             Log("Recalculating {0}", site.Url);
 120:             site.RecalculateStorageUsed();
 121:             using (SPSite site2 = new SPSite(site.ID))
 122:                 Log("Storage updated from {0} to {1} (in bytes)\r\n", site.Usage.Storage.ToString(), site2.Usage.Storage.ToString());
 123:         }
 124:     }
 125: }

The help for the command is shown below:

C:\>stsadm -help gl-recalculateusage

stsadm -o gl-recalculateusage


Recalculates usage statistics for the given site(s).

Parameters:
        [-scope <Farm | WebApplication | Site>]
        [-url <url>]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-recalculateusage WSS v3, MOSS 2007 Released: 1/15/2009

Parameter Name Short Form Required Description Example Usage
url   Yes if scope is not Farm URL of the web application or site collection. -url http://portal
scope s No – defaults to site The scope to use.  Valid values are “Farm”, “WebApplication”, and “Site” -scope site

-s site

The following is an example of how to recalculate the usage statistics for all the site collections within the farm:

stsadm -o gl-recalculateusage -scope farm

The following is an example of the output you might see after running the above command:

C:\>stsadm -o gl-recalculateusage -scope farm

Recalculating http://mysites
Usage updated from 425744 to 425744

Recalculating http://mysites/personal/spadmin
Usage updated from 588790 to 588790

Recalculating http://portal
Usage updated from 3249962 to 3249962

Recalculating http://sspadmin/ssp/admin
Usage updated from 642686 to 642686

Recalculating http://sharepoint1:1234
Usage updated from 18244961 to 18284043

Operation completed successfully.

If you wanted to do this exact same thing using PowerShell you could do the following:

PS W:\> foreach ($site in Get-SPSite -url *) {
>> $origStorage = $site.SPBase.Usage.Storage
>> $site.SPBase.RecalculateStorageUsed()
>> $site.SPBase.Dispose()
>> $tempSite = $site.GetSPObject()
>> $newStorage = $tempSite.Usage.Storage
>> Write-Host $tempSite.Url Updated from $origStorage to $newStorage
>> $tempSite.Dispose()
>> }
>>
http://mysites Updated from 425744 to 425744
http://mysites/personal/spadmin Updated from 588790 to 588790
http://portal Updated from 3249962 to 3249962
http://sspadmin/ssp/admin Updated from 642686 to 642686
http://sharepoint1:1234 Updated from 18284043 to 18284043
PS W:\>

Note that I used the SPBase property initially to get the current storage and then to call the RecalculateStorageUsed method.  You then must dispose of this object.  I then used the GetSPObject() method to get a new copy of the SPSite object because I can’t use the original copy as the usage data would be cached.  I can now use this new object to get the new usage data which I then write to the host and then I’m free to dispose of the object.  Also, because the SPSiteInfo objects that are returned by the Get-SPSite cmdlet do not require disposal you can easily pass the filter the results of that command before looping through the returned collection without worrying about disposing of items.

6Aug/0823

Delete All Users from a Site Collection

I was chatting with my buddy and fellow MVP, Todd Klindt, the other day and he was asking if I had a custom command that would allow him to quickly delete all the users added to a site collection.  For his purposes he needed this to troubleshoot an issue he was having with a site collection that contained many thousands of users.

I figured this would be pretty easy to do - a simple while loop with a conditional check for site administrators to prevent them from being deleted so I quickly typed out the following code during our IM conversation:

using (SPSite site = new SPSite(url))
using (SPWeb web = site.OpenWeb())
{
    int offsetIndex = 0;
    while (web.SiteUsers.Count > offsetIndex)
    {
 
        if (web.SiteUsers[offsetIndex].IsSiteAdmin || web.SiteUsers[offsetIndex].ID == web.CurrentUser.ID)
        {
            offsetIndex++;
            continue;
        }
        web.SiteUsers.Remove(offsetIndex);
    }
}

We can't use a for loop for this because we're modifying the collection and that would result in an error so we need to use a while loop and just keep deleting until there's no more to delete.  But, we have to watch out for site administrators as we can't directly delete them in this fashion (and it's a good idea to keep them regardless) so we use an offset that gets incremented whenever we hit an administrator.  We also can't delete the current user so I check for that as well.

One thing to note is that if you have a lot of users (Todd was dealing with 17,000) then it will run real slow - he was seeing about 300 deleted per hour with progressively more as the size dwindled - so be patient :)

I figured since I had the code that I might as well turn it into a new stsadm command so I created gl-deleteallusers.  There's not really much to this command - it just takes in a single URL parameter which points to your site collection.  I thought about adjusting this so that you could delete users from a specific web that had broken inheritance but I didn't really have time.  Here's the help output:

C:\>stsadm -help gl-deleteallusers

stsadm -o gl-deleteallusers


Deletes all site collection users.  Will not delete site administrators.

Parameters:
        -url <site collection url>

The following tables summarizes the command and its parameters:

Command Name Availability Build Date
gl-deleteallusers WSS v3, MOSS 2007 7/23/2008

Parameter Name Short Form Required Description Example Usage
url   Yes The URL of the site collection of which the users will be deleted.  If a sub-site of the site collection is passed in it will only look at the site collection itself and ignore the sub-site information. -url http://portal/

Using the command is real simple - the following will delete all users, except site administrators, from the http://portal/sites/site1 site collection:

stsadm -o gl-deleteallusers -url http://portal/sites/site1

6Mar/089

Set Anonymous Access

If you're trying to configure a site for anonymous access via stsadm you can get most of the way using the out of the box "extendvs" (or my createwebapp) and "authentication" commands. The problem is that this only enables the web application for anonymous access but it doesn't "turn it on" at the site collection or web level. To complete the process I created a new command: gl-setanonymousaccess. There are three levels that you can set: Entire Web, Lists and Libraries, and Nothing. If you set the access to Entire Web anonymous users will be able to view all pages in your site and view all lists and items which inherit permissions from the site. If you select Lists and Libraries anonymous users will be able to view and change items only for those lists and libraries that have enabled permissions for anonymous users. And of course, if you set the access to Nothing anonymous users will have no access. To make this change programmatically is really simple - it's just a matter of setting a single property on the web site of interest. I also added some additional code to enable breaking of the permissions inheritance if necessary:

   1:  public override int Run(string command, StringDictionary keyValues, out string output)
   2:  {
   3:      output = string.Empty;
   4:   
   5:      InitParameters(keyValues);
   6:   
   7:      string url = Params["url"].Value.TrimEnd('/');
   8:      WebAnonymousState state = (WebAnonymousState)Enum.Parse(typeof (WebAnonymousState), Params["anonstate"].Value, true);
   9:   
  10:      using (SPSite site = new SPSite(url))
  11:      using (SPWeb web = site.OpenWeb(Utilities.GetServerRelUrlFromFullUrl(url)))
  12:      {
  13:          if (!web.HasUniqueRoleAssignments && Params["enableuniquepermissions"].UserTypedIn)
  14:          {
  15:              web.BreakRoleInheritance(true);
  16:              web.Update();
  17:          }
  18:   
  19:          if (state == WebAnonymousState.EntireWeb)
  20:              web.AnonymousState = SPWeb.WebAnonymousState.On;
  21:          else if (state == WebAnonymousState.ListsAndLibraries)
  22:              web.AnonymousState = SPWeb.WebAnonymousState.Enabled;
  23:          else
  24:              web.AnonymousState = SPWeb.WebAnonymousState.Disabled;
  25:   
  26:          web.Update();
  27:      }
  28:   
  29:      return 1;
  30:  }
The syntax of the command can be seen below:
C:\>stsadm -help gl-setanonymousaccess

stsadm -o gl-setanonymousaccess

Sets the anonymous access settings for a given site collection.

Parameters:
        -url <url containing the content types>
        -anonstate <entireweb | listsandlibraries | none>
        [-enableuniquepermissions (breaks permissions inheritance for the web if not already broken)]
Here's an example of how to enable anonymous access for the entire web:
stsadm -o gl-setanonymousaccess -url "http://intranet" -anonstate entireweb

31Jan/0834

Create Site in Database

So you've got a content database (perhaps you used the gl-createcontentdb command) and now you want to create a site collection in that database. Problem is that you can't do this easily via the browser and you definitely can't do it via stsadm. There is a createsiteinnewdb command and a createsite command but the first will create a new database and the second will put the site collection in the "best match" database. But you need it in a specific database. What I decided to do was to create a new command: gl-createsiteindb.

The code is pretty straight forward - the bulk of the code is just validation code:

   1: using System;
   2: using System.Collections.Specialized;
   3: using System.IO;
   4: using System.Text;
   5: using System.Web.Configuration;
   6: using Lapointe.SharePoint.STSADM.Commands.SPValidators;
   7: using Microsoft.SharePoint;
   8: using Microsoft.SharePoint.Administration;
   9: using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
  10:  
  11: namespace Lapointe.SharePoint.STSADM.Commands.SiteCollectionSettings
  12: {
  13:     public class CreateSiteInDB : SPOperation
  14:     {
  15:         protected SPWebApplication m_WebApplication;
  16:  
  17:         /// <summary>
  18:         /// Initializes a new instance of the <see cref="CreateSiteInDB"/> class.
  19:         /// </summary>
  20:         public CreateSiteInDB()
  21:         {
  22:             SPParamCollection parameters = new SPParamCollection();
  23:             parameters.Add(new SPParam("url", "url", true, null, new SPUrlValidator()));
  24:             parameters.Add(new SPParam("lcid", "lcid", false, "0", new SPRegexValidator("^[0-9]+$")));
  25:             parameters.Add(new SPParam("sitetemplate", "st", false, null, new SPNullOrNonEmptyValidator()));
  26:             parameters.Add(new SPParam("title", "t", false, null, null));
  27:             parameters.Add(new SPParam("description", "desc", false, null, null));
  28:             parameters.Add(new SPParam("ownerlogin", "ol", false, null, new SPNonEmptyValidator()));
  29:             parameters.Add(new SPParam("ownername", "on", false, null, null));
  30:             parameters.Add(new SPParam("owneremail", "oe", true, null, new SPRegexValidator(@"^[^ \r\t\n\f@]+@[^ \r\t\n\f@]+$")));
  31:             parameters.Add(new SPParam("quota", "quota", false, null, new SPNullOrNonEmptyValidator()));
  32:             parameters.Add(new SPParam("hostheaderwebapplicationurl", "hhurl", false, null, new SPUrlValidator()));
  33:             parameters.Add(new SPParam("secondarylogin", "sl", false, null, new SPNullOrNonEmptyValidator()));
  34:             parameters.Add(new SPParam("secondaryname", "sn", false, null, null));
  35:             parameters.Add(new SPParam("secondaryemail", "se", false, null, null));
  36:             parameters.Add(new SPParam("dbname", "db", true, null, new SPNonEmptyValidator(), "Please specify the database name."));
  37:  
  38:             StringBuilder sb = new StringBuilder();
  39:             sb.Append("\r\n\r\nCreates a new site collection in an existing content database.\r\n\r\nParameters:");
  40:             sb.Append("-url <url>");
  41:             sb.Append("\r\n\t-owneremail <someone@example.com>");
  42:             sb.Append("\r\n\t[-ownerlogin <DOMAIN\\name>]");
  43:             sb.Append("\r\n\t[-ownername <display name>]");
  44:             sb.Append("\r\n\t[-secondaryemail <someone@example.com>]");
  45:             sb.Append("\r\n\t[-secondarylogin <DOMAIN\\name>");
  46:             sb.Append("\r\n\t[-secondaryname <display name>]");
  47:             sb.Append("\r\n\t[-lcid <language>]");
  48:             sb.Append("\r\n\t[-sitetemplate <site template>]");
  49:             sb.Append("\r\n\t[-title <site title>]");
  50:             sb.Append("\r\n\t[-description <site description>]");
  51:             sb.Append("\r\n\t[-hostheaderwebapplicationurl <web application url>]");
  52:             sb.Append("\r\n\t[-quota <quota template>]");
  53:             sb.Append("\r\n\t-dbname <content database name>");
  54:             
  55:             Init(parameters, sb.ToString());
  56:         }
  57:  
  58:         #region ISPStsadmCommand Members
  59:  
  60:         /// <summary>
  61:         /// Gets the help message.
  62:         /// </summary>
  63:         /// <param name="command">The command.</param>
  64:         /// <returns></returns>
  65:         public override string GetHelpMessage(string command)
  66:         {
  67:             return HelpMessage;
  68:         }
  69:  
  70:         /// <summary>
  71:         /// Runs the specified command.
  72:         /// </summary>
  73:         /// <param name="command">The command.</param>
  74:         /// <param name="keyValues">The key values.</param>
  75:         /// <param name="output">The output.</param>
  76:         /// <returns></returns>
  77:         public override int Execute(string command, StringDictionary keyValues, out string output)
  78:         {
  79:             output = string.Empty;
  80:  
  81:             
  82:  
  83:             string dbname = Params["dbname"].Value;
  84:  
  85:             string uriString = Params["url"].Value;
  86:             string webTemplate = Params["sitetemplate"].Value;
  87:             string title = Params["title"].Value;
  88:             string description = Params["description"].Value;
  89:             string ownerName = Params["ownername"].Value;
  90:             string ownerEmail = Params["owneremail"].Value;
  91:             string quota = Params["quota"].Value;
  92:             string secondaryContactName = Params["secondaryname"].Value;
  93:             string secondaryContactEmail = Params["secondaryemail"].Value;
  94:             bool isHostHeaderWebAppUrlTypedIn = Params["hostheaderwebapplicationurl"].UserTypedIn;
  95:             uint nLCID = uint.Parse(Params["lcid"].Value);
  96:             Uri uri = new Uri(uriString);
  97:             SPUrlZone zone = SPUrlZone.Default;
  98:             bool createActiveDirectoryAccounts = m_WebApplication.WebService.CreateActiveDirectoryAccounts;
  99:             string ownerLogin = Utilities.TryGetNT4StyleAccountName(Params["ownerlogin"].Value, m_WebApplication);
 100:             string secondaryContactLogin = null;
 101:             if (!createActiveDirectoryAccounts || !Params["secondaryemail"].UserTypedIn)
 102:             {
 103:                 if (!createActiveDirectoryAccounts && Params["secondarylogin"].UserTypedIn)
 104:                 {
 105:                     secondaryContactLogin = Utilities.TryGetNT4StyleAccountName(Params["secondarylogin"].Value, m_WebApplication);
 106:                 }
 107:             }
 108:             else
 109:             {
 110:                 secondaryContactLogin = @"@\@";
 111:             }
 112:  
 113:             SPContentDatabase database = null;
 114:             foreach (SPContentDatabase tempDB in m_WebApplication.ContentDatabases)
 115:             {
 116:                 if (tempDB.Name.ToLower() == dbname.ToLower())
 117:                 {
 118:                     database = tempDB;
 119:                     break;
 120:                 }
 121:             }
 122:             if (database == null)
 123:                 throw new SPException("Content database not found.");
 124:  
 125:             if (database.MaximumSiteCount <= database.CurrentSiteCount)
 126:                 throw new SPException("The maximum site count for the specified database has been exceeded.  Increase the maximum site count or specify another database.");
 127:  
 128:             SPSite site = null;
 129:             try
 130:             {
 131:                 site = database.Sites.Add(uri.OriginalString, title, description, nLCID, webTemplate, ownerLogin,
 132:                     ownerName, ownerEmail, secondaryContactLogin, secondaryContactName, secondaryContactEmail,
 133:                     isHostHeaderWebAppUrlTypedIn);
 134:  
 135:  
 136:                 if (!string.IsNullOrEmpty(quota))
 137:                 {
 138:                     using (SPSiteAdministration administration = new SPSiteAdministration(site.Url))
 139:                     {
 140:                         SPFarm farm = SPFarm.Local;
 141:                         SPWebService webService = farm.Services.GetValue<SPWebService>("");
 142:  
 143:                         SPQuotaTemplateCollection quotaColl = webService.QuotaTemplates;
 144:                         administration.Quota = quotaColl[quota];
 145:                     }
 146:                 }
 147:                 if (!string.IsNullOrEmpty(webTemplate))
 148:                 {
 149:                     using (SPWeb web = site.RootWeb)
 150:                     {
 151:                         web.CreateDefaultAssociatedGroups(ownerLogin, secondaryContactLogin, string.Empty);
 152:                     }
 153:                 }
 154:                 if (isHostHeaderWebAppUrlTypedIn && !m_WebApplication.IisSettings[zone].DisableKerberos)
 155:                 {
 156:                     Console.WriteLine(SPResource.GetString("WarnNoDefaultNTLM", new object[0]));
 157:                     Console.WriteLine();
 158:                 }
 159:             }
 160:             finally
 161:             {
 162:                 if (site != null)
 163:                     site.Dispose();
 164:             }
 165:  
 166:             return OUTPUT_SUCCESS;
 167:         }
 168:  
 169:         /// <summary>
 170:         /// Validates the specified key values.
 171:         /// </summary>
 172:         /// <param name="keyValues">The key values.</param>
 173:         public override void Validate(StringDictionary keyValues)
 174:         {
 175:             base.Validate(keyValues);
 176:             string uriString = Params["url"].Value;
 177:  
 178:             if (Params["quota"].UserTypedIn)
 179:             {
 180:                 string quota = Params["quota"].Value;
 181:                 SPFarm farm = SPFarm.Local;
 182:                 SPWebService webService = farm.Services.GetValue<SPWebService>("");
 183:  
 184:                 SPQuotaTemplateCollection quotaColl = webService.QuotaTemplates;
 185:  
 186:                 if (quotaColl[quota] == null)
 187:                 {
 188:                     throw new ArgumentException(SPResource.GetString("InvalidQuotaTemplateName", new object[0]));
 189:                 }
 190:             }
 191:  
 192:             m_WebApplication = ValidateWebApplication(this);
 193:             if (m_WebApplication == null)
 194:             {
 195:                 throw new FileNotFoundException(SPResource.GetString("WebApplicationLookupFailed", new object[] { uriString }));
 196:             }
 197:             SPUrlZone zone = SPUrlZone.Default;
 198:             if (!m_WebApplication.WebService.CreateActiveDirectoryAccounts && !Params["ownerlogin"].UserTypedIn)
 199:             {
 200:                 throw new ArgumentException(SPResource.GetString("NoADCreateRequiresOwnerLogin", new object[0]));
 201:             }
 202:             if (m_WebApplication.GetIisSettingsWithFallback(zone).AuthenticationMode == AuthenticationMode.Windows)
 203:             {
 204:                 bool bIsUserAccount;
 205:                 if (Params["ownerlogin"].UserTypedIn)
 206:                 {
 207:                     string strLoginName = Utilities.TryGetNT4StyleAccountName(Params["ownerlogin"].Value, m_WebApplication);
 208:                     if (!Utilities.IsLoginValid(strLoginName, out bIsUserAccount))
 209:                     {
 210:                         throw new ArgumentException(SPResource.GetString("InvalidLoginAccount", new object[] { strLoginName }));
 211:                     }
 212:                     if (!bIsUserAccount)
 213:                     {
 214:                         throw new ArgumentException(SPResource.GetString("OwnerNotUserAccount", new object[0]));
 215:                     }
 216:                 }
 217:                 if (Params["secondarylogin"].UserTypedIn)
 218:                 {
 219:                     string strLoginName = Utilities.TryGetNT4StyleAccountName(Params["secondarylogin"].Value, m_WebApplication);
 220:                     if (!Utilities.IsLoginValid(strLoginName, out bIsUserAccount))
 221:                     {
 222:                         throw new ArgumentException(SPResource.GetString("InvalidLoginAccount", new object[] { strLoginName }));
 223:                     }
 224:                     if (!bIsUserAccount)
 225:                     {
 226:                         throw new ArgumentException(SPResource.GetString("OwnerNotUserAccount", new object[0]));
 227:                     }
 228:                 }
 229:             }
 230:         }
 231:  
 232:  
 233:  
 234:  
 235:         #endregion
 236:  
 237:         internal static SPWebApplication ValidateWebApplication(SPOperation operation)
 238:         {
 239:             if (SPFarm.Local == null)
 240:             {
 241:                 throw new SPException(SPResource.GetString("OperationInvalidInRemoteFarm", new object[0]));
 242:             }
 243:             SPWebApplication application;
 244:             string uriString = operation.Params["url"].Value;
 245:             bool isHostHeaderWebAppUrlTypedIn = operation.Params["hostheaderwebapplicationurl"].UserTypedIn;
 246:             Uri requestUri = new Uri(uriString);
 247:             if ((requestUri.Scheme != Uri.UriSchemeHttps) && (requestUri.Scheme != Uri.UriSchemeHttp))
 248:             {
 249:                 throw new ArgumentException(SPResource.GetString("InvalidSiteName", new object[] { uriString }));
 250:             }
 251:             if (isHostHeaderWebAppUrlTypedIn)
 252:             {
 253:                 Uri hostHeaderWebAppUrl = new Uri(operation.Params["hostheaderwebapplicationurl"].Value);
 254:                 application = SPWebApplication.Lookup(hostHeaderWebAppUrl);
 255:                 if (application == null)
 256:                 {
 257:                     return application;
 258:                 }
 259:                 bool portMatchFound = false;
 260:  
 261:                 foreach (SPIisSettings iisSettings in application.IisSettings.Values)
 262:                 {
 263:                     foreach (SPSecureBinding secureBinding in iisSettings.SecureBindings)
 264:                     {
 265:                         if (requestUri.Port == secureBinding.Port)
 266:                         {
 267:                             portMatchFound = true;
 268:                             break;
 269:                         }
 270:                     }
 271:  
 272:                     if (!portMatchFound)
 273:                     {
 274:                         foreach (SPServerBinding serverBinding in iisSettings.ServerBindings)
 275:                         {
 276:                             if (requestUri.Port == serverBinding.Port)
 277:                             {
 278:                                 portMatchFound = true;
 279:                                 break;
 280:                             }
 281:                         }
 282:                     }
 283:                     if (portMatchFound)
 284:                         return application;
 285:                 }
 286:                 Console.WriteLine(SPResource.GetString("HostHeaderDoesNotMatchWebAppPort", new object[0]));
 287:                 Console.WriteLine();
 288:             }
 289:             else
 290:                 return SPWebApplication.Lookup(requestUri);
 291:  
 292:             return application;
 293:         }
 294:  
 295:     }
 296: }

The syntax of the command can be seen below:

C:\>stsadm -help gl-createsiteindb

stsadm -o gl-createsiteindb

Creates a new site collection in an existing content database.

Parameters:
        -url <url>        
        -owneremail <someone@example.com>        
        [-ownerlogin <DOMAIN\\name>]        
        [-ownername <display name>]        
        [-secondaryemail <someone@example.com>]        
        [-secondarylogin <DOMAIN\\name>        
        [-secondaryname <display name>]        
        [-lcid <language>]        
        [-sitetemplate <site template>]        
        [-title <site title>]        
        [-description <site description>]        
        [-hostheaderwebapplicationurl <web application url>]        
        [-quota <quota template>]        
        -dbname <content database name>

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-createsiteindb WSS 3, MOSS 2007 Released: 1/31/2008
Updated: 4/30/2009 

Parameter Name Short Form Required Description Example Usage
url   Yes The URL to the site collection to create. -url "http://portal/sites/NewSite"
owneremail oe Yes

The site owner's e-mail address.  Must be valid e-mail address, in the form someone@example.com.

-owneremail someone@example.com

-oe someone@example.com
ownerlogin ol

If your farm does not have Active Directory account creation mode enabled, then this parameter is required.

This parameter should not be provided if your farm has Active Directory account creation mode enabled, as Microsoft Office SharePoint Server 2007 will automatically create a site collection owner account in Active Directory based on the owner e-mail address.

The site owner's user account.  Must be a valid Windows user name, and must be qualified with a domain name, for example, domain\name

-ownerlogin domain\name

-ol domain\name
ownername on No

The site owner's display name.

-ownername "Gary Lapointe"

-on "Gary Lapointe"
secondaryemail se No

The secondary site owner's e-mail address.  Must be valid e-mail address, in the form someone@example.com.

-secondaryemail someone@example.com

-se someone@example.com
secondarylogin sl

If your farm does not have Active Directory account creation mode enabled, then this parameter is required.

This parameter should not be provided if your farm has Active Directory account creation mode enabled, as Microsoft Office SharePoint Server 2007 will automatically create a site collection owner account in Active Directory based on the owner e-mail address.

The secondary site owner's user account.  Must be a valid Windows user name, and must be qualified with a domain name, for example, domain\name

-secondarylogin domain\name

-sl domain\login
secondaryname sn No

The secondary site owner's display name.

-secondaryname "Pam Lapointe"

-sn "Pam Lapointe"
lcid   No

A valid locale ID, such as "1033" for English.  You must specify this parameter when using a non-English template.

-lcid 1033
sitetemplate st No

Specifies the type of template to be used by the newly created site.

The value must be in the form name#configuration. If you do not specify the configuration, configuration 0 is the default (for example, STS#0). The list of available templates can be customized to include templates you create.

-sitetemplate STS#0

-st STS#0
title t No

The title of the new site collection

-title "New Site"
description desc No

Description of the site collection.

-description "New Site Description"

-desc "New Site Description"
hostheaderwebapplicationurl hhurl No

A valid URL assigned to the Web application by using Alternate Access Mapping (AAM), such as "http://server_name".

When the hostheaderwebapplicationurl parameter is present, the value of the url parameter is the URL of the host-named site collection and value of the hostheaderwebapplicationurl parameter is the URL of the Web application that will hold the host-named site collection.

-hostheaderwebapplicationurl http://newsite

-hhurl http://newsite
quota   No

The quota template to apply to sites created on the virtual server.

-quota Portal
dbname db Yes

The name of the Microsoft SQL Server database or Microsoft SQL Server 2000 Desktop Engine (Windows) (WMSDE) database used for Windows SharePoint Services data.

-dbname SharePoint_Content1

-db SharePoint_Content1

Here's an example of how to create a site in an existing content database:

stsadm -o gl-createsiteindb -url "http://intranet/sites/testsite1" -owneremail "someone@domain.com" -ownerlogin "domain\someone" -ownername "Some User" -sitetemplate "BLANKINTERNET#2" -title "Test Site" -dbname "SharePoint_ContentDB1"

Update 4/30/2009: I’ve updated the code so that it now creates the default site groups for the site collection.

28Jan/084

Set the Welcome Page for a Web

When I did the gradual upgrade I found that several sites had the welcome page set to UpgLandingPgRedir.aspx when it should have been set to default.aspx. I didn't have time to figure out why it was doing this but the fix was easy enough - I just had to change the welcome page back. To do this I created a new command: gl-sitewelcomepage. The code to set this is really simple - basically just get a PublishingWeb instance and set the DefaultPage property to a valid SPFile object:

   1: string url = Params["site"].Value.TrimEnd('/');
   2: string page = SPHttpUtility.UrlPathDecode(Params["welcomepage"].Value, true);
   3:  
   4: using (SPSite site = new SPSite(url))
   5: using (SPWeb web = site.AllWebs[Utilities.GetServerRelUrlFromFullUrl(url)])
   6: {
   7:  if (!PublishingWeb.IsPublishingWeb(web))
   8:   throw new SPException("The specified site is not a publishing web.");
   9:  
  10:  PublishingWeb pubWeb = PublishingWeb.GetPublishingWeb(web);
  11:  
  12:  bool flag = false;
  13:  SPFile file = null;
  14:  try
  15:  {
  16:   file = pubWeb.Web.GetFile(page);
  17:   flag = file.Exists;
  18:  }
  19:  catch (SPException)
  20:  {
  21:   flag = false;
  22:  }
  23:  catch (FileNotFoundException)
  24:  {
  25:   flag = false;
  26:  }
  27:  catch (ArgumentException)
  28:  {
  29:   flag = false;
  30:  }
  31:  
  32:  if (!flag || file == null)
  33:   throw new SPException("The specified welcome page could not be found.");
  34:  
  35:  
  36:  PublishingWeb currentPublishingWeb = pubWeb;
  37:  currentPublishingWeb.DefaultPage = file;
  38:  currentPublishingWeb.Update();
  39: }

The syntax of the command can be seen below:

C:\>stsadm -help gl-sitewelcomepage

stsadm -o gl-sitewelcomepage

Sets the page to be used as the welcome page for the site.

Parameters:
        -site <url of the site to update>
        -welcomepage <full url to the page to use as the welcome page>
Here's an example of how to set the welcome page for a site:
stsadm -o gl-sitewelcomepage -site "http://intranet/sites/teamsite1" -welcomepage "http://intranet/sites/teamsite1/pages/default.aspx"